Things I would change about React to increase DX

Things I would change about React to increase DX

Posted 2023-05-14

I love React. I use React daily and generally think it is good at what it does. We can build extremely complex FE applications with it.

I also know that a lot of hard work and a lot of thinking goes into any new feature that is released.

This is just a hypothetical blog post, with some ideas of things that I would implement. And is in no way an article against using React.

I am very aware of the reasons why these ideas are not practical for React - I am not going to concentrate on how feasible these things are. I can see a bunch of blockers to several of these ideas (although I am sure there are smart people would could work around them). Some of these features are stolen from other FE libraries/frameworks.

Better ways to decide if we should re-run a useEffect

Hopefully, everyone is using eslint-plugin-react-hooks, which will warn us if we are missing a dependency.

Sometimes you truly don't want to include a specific dependency (please: never aim for this!), and you end up adding // eslint-disable-next-line react-hooks/exhaustive-deps.

But if you are not careful, you can make another change within useEffect and you should have added a new dependency. But as you disabled eslint (for all dependency checks) it won't tell you and you can easily introduce a bug.

Something like this would be better, and we could use eslint to check all required deps:

✅ copied
// hypothetical example - this will not work! useEffect(() => { if(depA) { doSomething(); console.log(`Dep A is in state x - debug info: ${depB}`) } }, { required: [depA], ignore: [depB], // don't re-render when depB changes... but don't require ignoring all deps in the eslint rule })

As I write this out, I can see that getting a bit annoying in a real work environment. But something like that basic idea could mean we could remove the need to disable all dependency checks and be explicit about what we want to ignore.

Classname in JSX

I know class is a protected keyword in JS (const class = 'x' is not valid JS). I've read many times that this is why we must use className. But I don't even think that is true (as you can set it as a property key in an object, just not as a variable name).

The property className is the property name in the Web API (see MDN. So I understand why we use className in JSX.

But I'd love to be able to just do class="..."

// instead of: <div className="border">...</div> // I would prefer: <div class="border">...</div>

Although at this stage even if I write plain HTML I end up accidentally writing className= there too...

Better ways to fetch data built into React

If you have a React app and don't use a data fetching library like React Query or RTK Query then you are probably familiar with data fetching patterns like:

✅ copied
function Component() { const [data, setDate] = useState() useEffect(() => { // simplified example, fetch data from API then set it // as state data. fetchDataFromYourApi() .then(result => setData(result)) }, []) return <div>...</div> }

And then something which trips everyone up the first time they use React - <StrictMode>'s re-running of useEffects.

Even without strict mode, this sort of boilerplate is problematic...

The intent from the developer is "Loading X data once". But we have to wrap it in useEffect, and it is hard to persist between different components/unmounting/remounting. We also have to manually check that the data is loaded only once, and handle error states (neither are shown in my example)

In the old days of React (class-based), it was a bit easier to put this sort of logic in componentDidMount. At least it was clearer to new engineers how it runs, without the useEffect() deps list of [].

It would be great if React had a nice declarative API to initially load data in a smart way.

Of course, nowadays I would strongly recommend using a dedicated data fetching library - TanstackQuery, SWR or RTK Query (especially if you use Redux anyway).

And frameworks like Next or RSC change the model upside down a little. But data fetching is core to almost every client side application so bundling it as part of core React could save a lot of hassle.

Better reactivity

I love Vue's reactivity system. They use JS Proxy objects and between each render, it tracks what variables were accessed (or updated), and builds up a dependency graph between reactive variables. Similar in a way to SolidJS's approach.

I won't explain how it works (that can be a different article) as it takes a while to explain how it is implemented, but it's a completely different system than React's way of handling state changes (and this is why you cannot run hooks after/within if branches).

For some reason in 2023 'signals' became very popular on Twitter. I haven't fully explored that library, but the basic idea looks similar to Vue. So there is growing interest in other ways of doing reactivity...

Maybe it wouldn't be so hard to bring it into React. But at this stage, it would add another level of complexity to React so I guess I can settle and be content that state changes in hooks are here to stay.

React - but written in TypeScript

There is a lot of history with how typing was/is done in React. They use flow. I have heard rumors that moving to typescript is on the table but I am not sure how true it is.

It's not a huge deal as the @types/react are always up to date. But I think the entire web ecosystem is moving towards typescript everywhere. I would just like to be able to easily see TS code in the library.

There are probably many reasons that I haven't thought of, which keep the React code base in JS. At the end of the day, this has very little impact to anyone using React, as we just need the .d.ts @types files (which we have).

Simplify the React typing system

While on the topic of TypeScript... It would be good to simplify the huge number of available types.

They all serve a different purpose, so I guess we can't (and shouldn't) get rid of any.

But these are some of the harder ones to remember:

  • React.ReactNode vs React.JSX.Element
  • React.ComponentType vs React.ComponentProps
  • All the ones like React.InputHTMLAttributes<HTMLInputElement> or React.SVGProps<SVGSVGElement>

I often see blog posts or Twitter (especially from @mattpocockuk) showing snippets explaining the easy/correct way to type things in React. And the reason for this is because it is hard to pick the most suitable one.

I never see similar tweets (x-s?) about how to type other commonly used things.

I can never remember which one to use due to how many types there are...

Better handling of generating conditional class names

In React/JSX, the class property on DOM elements must be written like <div className="flex mb-0">, and you cannot pass in an object/array containing multiple classes. You must turn whatever your class name config is into a plain string.

If you want to conditionally set some classes, you often end up doing something like this:

const classes = `flex ${size === 'Large' ? 'h-10' : ''} `; return <div className={classes} />

I know most React applications use something like clsx to generate class name strings.

This works like this (check their docs for more complex examples):

✅ copied
const classes = clsx({ flex: true, 'h-10': size === 'Large' }) return <div className={classes} />

But why can't we have it built into JSX?

It is very common to conditionally set/remove some classes based on other variables.

Having it built into React (JSX) would make the code a tiny bit cleaner.

Better handling of inline style

This is a similar point to the previous one, but manually setting style is great if you want to create an object:

✅ copied
const divStyle = { color: 'red', }; function YourComponent() { return <div style={divStyle} /> }

But there are times when I've wanted to just pass in a string:

function YourComponent() { return <div style="color: red;" /> }

From a DX perspective, it would be great to be able to do this. From a practical standpoint, the only times I've wanted to do this is when mocking things up or with demo applications.

Automatic useCallback...

I think this is coming ('React forget') but there are two reasons useCallback sucks:

  • it is easy to forget to wrap a function in useCallback, pass it as a dependency and not realise it is triggering too many re-renders or useEffect calls
    • It is also sometimes tough to notice in a PR review that a prop passed down is not a stable function (via useCallback)
  • It is just more boilerplate to always have to write it out.

But I don't think we can get this resolved without React turning itself into more of a compiler.

Remove the need for {' '}

I wish JSX would somehow be more intelligent and remove the need for {' '}! Just let it figure out if we need a space or not.

I'm sure this would cause more problems than it solves, and make some HTML output more awkward to write.

If I am writing this (with spans), in this particular example I want the whitespace (so it renders as ("1 2 3")). But it renders as "123".

✅ copied
function NoWhitespace() { return <div> <span>1</span> <span>2</span> <span>3</span> </div> }

There are cases it does work correctly, for example adding the spaces around <b> here:

function Whitespace() { return <div>Hello I am <b>sure</b> that I want space</div> }

But if you format that differently, then it renders as `Hello I am surethat I want a space':

✅ copied
function BrokenWhitespace() { return <div> Hello I am <b>sure</b> that I want space </div> }

I think this one might be impossible to implement without introducing a lot of bugs/issues. It is easier to trim white space and add it when required than try to add it (how would we tell react to remove the white space...).

(Note: it did add whitespace in earlier versions of React, then they added the current implementation in v0.9.)

Somehow replace ErrorBoundary (getDerivedStateFromError) with a more hook-based approach

I have no idea how this could be done - but having a modern app full of hooks and then having just those one or two components that still cling on to class-based components isn't nice and can be confusing for anyone who learned React since hooks became the primary way to write components.

It's not a huge deal. It works, so why fix it? I am not suggesting that we should get rid of class-based components (as it would break backward compatibility too much).

I guess if I worked on an app that was fully class-based, this wouldn't look so alien to me nowadays.

✅ copied
class ErrorBoundary extends React.Component { constructor(props) { super(props); this.state = { hasError: false }; } static getDerivedStateFromError(error) { return { hasError: true }; } componentDidCatch(error, info) { logErrorToMyService(error, info.componentStack); } render() { if (this.state.hasError) { return this.props.fallback; } return this.props.children; } }

Better debugging messages

Sometimes the debug messages are clear. They show what file and what line.

And then there are the times with 30 lines of errors, with every file from ./node_modules/ and not a single one from your file (often with async functionality).

This would be a great DX improvement. But it's a very difficult issue to resolve (and not specific to React).

Better ways to be able to tell why something re-rendered

You can sometimes kind of tell in the React Developer Tools why a component re-rendered. But it is in my experience not always easy to figure out.

If we could have a way to say tell me why this useEffect is re-running, with a nice tree of parent calls it would be a nice time saver and a great DX improvement.

Better testing methodologies

The main testing library for Vue (Vue Test Utils) is really pleasant work with async functionality*. You think in 'ticks'. It might not be as realistic as how we test in React, but it is much easier to think about in your head.

(or, maybe because I worked with Vue for many years before moving to React I just feel like it is a nicer way ;))

Testing async behavior in Vue works roughly like this:

✅ copied
import { nextTick } from 'vue' test('increments by 1', async () => { const wrapper = mount(Counter) expect(wrapper.html()).toContain('Count: 0') wrapper.find('button').trigger('click') await nextTick() // << the magic here expect(wrapper.html()).toContain('Count: 1') })

In React, even with the React Testing Library, we have a lot of awkward act() calls.

(Yes, we should avoid them. Using something like await screen.findByText('...') can often be used to avoid await act(() => ...)...)

Even with good practices, we are probably all familiar with this in our console when we miss something:

"Warning: An update to $ComponentName inside a test was not wrapped in act"

It (often) isn't difficult to get around this. But it isn't great DX especially for anyone not used to writing FE tests.

I've had to help explain many times to other engineers how it should work and every time it sounds like I'm explaining a badly designed system.

Despite this, I am a huge fan of writing FE tests, RTL is great. I just wish it was easier to onboard engineers into the world of writing FE tests.

A way to hook into JSX

If you want React to do its work, but before rendering make some changes to the DOM (e.g. rewrite class names at runtime) it is very difficult.

This is completely out of the scope of React, and it is possible with tools like Babel (you can see an example at babel-plugin-jsx-remove-data-test-id).

But there is very little way for a React app to hook into any of the core React functionality.

If you think about most complex libraries or frameworks there are often hooks or middleware to make custom changes to how it processes things. But we have none of that with React.

As an example, I'd love to be able to do something like this (with just React, no build tools like Babel):

✅ copied
// hypothetical example // will run before every element is rendered/generated const jsxMiddleware = (element) => { element.className = addCustomTransformClassnames(element.className) element.onClick = addCustomOnClickTracking(element.onClick) return element; } ReactDOM.render( yourApp, domContainer, jsxMiddleware // << pass in some way to hook into things... );

Adding some hooks/middleware like this would 100% be abused and cause many bugs and more complex applications, and probably huge performance issues.

But this page is a list of ideal features without any considerations for practicalities ;)

Subscribe to my
Full Stack Typescript Developer

If you enjoyed my content and want more Full Stack Typescript content, then enter your email below.

I send a few links every couple of weeks, that I personally found useful from around the web.

Focusing on Typescript, NextJS, React, and also
engineering soft skills posts.