September 06, 2024

React is easy (Csaba is dead)

99% of what you read and hear about react is dogmatic, religious almost fanatical garbage. React is a (html) rendering system, but more importantly it is an idea that with the powerful browser we have, we can simplify our dev lives.

How can we simplify?

  1. Having pure dumb data, eg maps (dictionaries) as shallow as possible. No getter or setters, not methods at all.
  2. Take the data and just render it. Everything, always, whether it’s the first time or in response to data changes.
  3. Break up your app into components.

fresh

It turns web dev into game dev. We could run the render 60frames/sec, but as an optimization we only render when stuff changes.

React makes the 1 part easy by providing a few abstractions to hide the data.
And it makes the 2 part work by some clever optimizations.
The 3 part is just common sense of divide and conquer, but let's start with it.

Components

If you are familiar with React skip this part. Also there are better and more in-depth explanations of how this work.

The core concept of react is a component.

Whats a component? It is a function, that gets some inputs from its parent called props and produces a tree of elements as output. So far so good? and what is an element you ask? Element is a simple object that has props, children elements (maybe empty) and a constructor: either a html element name (div) or a component - a function.

To get the ball rolling we have to give react one root element: the constructor, props and children.

Rendering

Conceptually rendering in react means walking the tree of elements and calling the constructor method (aka rendering the component aka render) to get the next level of elements or creating the html element if the constructor is just a html element name.

In real life, there is more going on, since we want to avoid rendering when not needed, we might want to associate some runtime data with each element that is preserved between renders and we want to react to events.

Only render changes

The way react avoids rendering is to only render if the props passed to a component changed. How does react know props changed? It keeps the element tree from the previous render around and checks whether the previous props properties are the same as the new ones. This is a fast check eg === , and it is shallow. That means if you pass an object in props, you have to clone it so that it will register as changed {…x}. And because it is shallow changes to x’s properties will not be detected!

Tree compare

How does react compare the previous and new element tree? It walks the new tree starting at the root. It compares the old root and if true we continue. Now it looks at the first child of the new root, and compares old root’s children to it one at the time until it finds one where compare is true. What is this magic compare? -> checks the constructor is the same and checks the key prop is the same. (This is super handy for lists but can be useful in other scenarios). If compare is true, react keeps the component instance (state), otherwise a new component instance is created (this is called mounting). If the element is new or props changed, the constructor is called and we continue with the children elements. Then we go to the next root child element and so forth. All elements in the old tree that have not compared true, (and were not skipped because they or the parents did not change) will be discarded (unmounted).

Once you understand the react rendering, you can game the system and for example provide explicit rendering hints to react, without mucking with immutability. But, you really should not, unless your profiler tells you too!

One global state

There is great freedom in the stateless nature of dumb data being turned into UI in a pure and predictable way. You don’t care about what happened before, how the data got this way or why. Your job is to render it.

Dumb data makes me look smart. Smart data makes me hunt bugs for days and makes my head hurt trying to visualize all the interactions and how they can go wrong.

Be productive, say no to smart data, active records and all that garbage.

Imaging you are building a game. The user's player shoots the gun. Transform your global world data that contains a bullet object (a simple map of properties) and just render.

But I need/want to have state in my child component… no you don't. There is only one reason to have state in a component that is not the root. And then there is one more reason why state is used, but its not really state.

Pure UI state

For example if your component is a combobox and has a popup element. You will need an open/closed state to keep track of the popup. And it is not necessary to keep this information in the global state. Since the open/close state is transient it does not need to be preserved. This is a rule of thumb, it might be that for your use-case the open/close state has to be part of the world data or available to the parent component.
In a game, we draw all the pixels, it's therefore hard to come up with a good analogy. Let's say we have a chat feature in our game. We paint a translucent stream of messages onto the game screen. From the point of view of our game's world data the chat windows doesn’t matter, the rendered game output is the same. Only at the very end when the game rendering is done the chat messages are drawn on top of the final rendered game bitmap. Or not - depending on the chat-window-open state.

Async cache

Let’s say we have a component that displays additional business data related to whatever props we get. For example on a customer page we have tabs that show customer details, list of visits, list of quotes and invoices. One options is to load all the related data (visits and quotes). This might work, but if the user does not look at the tab of visits or quotes, we just wasted resources loading data that is not needed and even more importantly we made the user wait while we load data she does not care about. So we only load the data when the react component is actually rendered and we cache the data so we don’t load it each time we render. If the data loading is synchronous we can just use the useMemo hook, but this is unlikely and we’ll need an asynchronous fetch to the server. The pattern is useEffect to start the loading and a useState to keep the data. We have omitted loading state and error handling for clarity. There is a subtle bug here. Can you spot it? We’ll talk about it later.

Naked useState is considered harmful

Component “instance data” (eg whatever we have in useState or useMemo or useRef) lifetime is not tied to props. When rendering react will compare the tree of elements rendered last time, with the elements rendered now. If the elements are the same they will get reused (and the component instance data lives on), otherwise the old component if any is un-mounted and the new component is mounted (with fresh empty instance data).

Usually this is great and it is the reason React is fast. Now consider this situation: we have an async cache loaded (2) with visits for customer Alice. But the user now selects the customer Bob. Our useEffect call will be called again, because we watch the customerId prop. But until the load is complete we will render alice’s visits! Now there is a reason why cache invalidation is considered a hard problem - there are many subtle situation we need to handle. So what can we do?

  1. useEffect clears the state first
  2. useEffect can return a cleanup method, we could clear the state. We will still render the wrong visits, but we will re-render quickly. Now this one is kinda gray, as the final unmount if the component will show a warning in the console.
  3. useDerivedState, this is similar to useState but it has an extra parameter. Similar to the monitor in useEffect or useMemo, it will do the state data init not just once, but each time monitor values change. Thus useState is equivalent to useDerivedState with [] as monitor). This way you dont need to wait for useEffect with an extra render and there is never stale data rendered at all.
import { useCallback, useRef, useState } from "react";

export const CustomerVisits = (props: {customerId: string}) => {

	//const [visits, setVisits] = useState([]);
	const [visits, setVisits] = useDerivedState([], [props.customerId]); // fix 3.

	useEffect(()=>{

		setVisits([]) // fix 2.

		const asyncLoad = async () => {
			const resp = await fetch("/visits/"+props.customerId);
			const newVisits = await resp.json();
			setVisits(newVisits);
		}
		asyncLoad();

	},[props.customerId]);

	return <div>
		{visits.map(visit=>{
			return <div>{visit.name} - {visit.date}</div>
		})}
	</div>
}

export const useRender = () => {
	const [_, r] = useState(0);
	const render = useCallback(() => {
		r(x => ((x + 1) & 0xffff));
	}, [r]); // do not just flip true/false. Double render in strict debug mode could flip the value back!
	return render;
}

export const useDerivedState = <T extends unknown>(initial: (T | (()=> T)), monitor: any[]): [T, (v: T | ((prev: T) => T)) => void] => {
	const r = useRef(monitor);
	const valueRef = useRef<T>(undefined as T);
	const forceRender = useRender();
	
	if (valueRef.current === undefined || !r.current.every((x, i) => Object.is(x, monitor[i]))) {
		if (typeof initial === "function")
			valueRef.current = (initial as (() => T))();
		else
			valueRef.current = initial as T;
		r.current = monitor;
		
	}
	const setValue = useCallback((value: T | ((prev: T) => T)) => {
		let newValue: any;
		if (typeof value === "function")
			newValue = (value as ((prev: T) => T))(valueRef.current);
		else
			newValue = value as T;
		if (!Object.is(valueRef.current, newValue)) {
			valueRef.current = newValue;
			forceRender();
		}
	}, [valueRef]);
	return [valueRef.current, setValue];
}

useEffect race condition

Consider the situation where we start loading alice’s visits, but the user changes to bob before we are done. Now we will clear the visits in useEffect but what if bob’s visits come sooner than alice’s? (Eg there is a race between alice and bob’s visits - so race condition). Well there is nothing that prevents this situation, therefore if the internet speeds align just right - we can show alice’s visits instead of bob’s!

Naive code

export const CustomerVisits = (props: {customerId: string}) => {

	const [visits, setVisits] = useState([]);

	useEffect(()=>{

		const asyncLoad = async () => {
			const resp = await fetch("/visits/"+props.customerId);
			const newVisits = await resp.json();
			setVisits(newVisits);
		}
		asyncLoad();

	},[props.customerId]);

	return <div>
		{visits.map(visit=>{
			return <div>{visit.name} - {visit.date}</div>
		})}
	</div>
}

Now that will not do, so how can we fix it? Remember there is a optional cleanup for useEffect - and we shall set a cancel flag there. While that is not strictly needed, we can also cancel a fetch in progress - handy if we are fetching some large pictures with visits…

Better code

{
	useEffect(()=>{
		const controller = new AbortController();
		let cancel = false;

		// fix 1. clear cache
		setVisits([]);

		const asyncLoad = async () => {
			// pass 
			try {
				const resp = await fetch("/visits/" + props.customerId, { signal: controller.signal });
				const newVisits = await resp.json();
				if (!cancel)
					setVisits(newVisits);
			}
			catch (e) {
				// aborted or other error 
			}
		}
		asyncLoad();

		// cleanup 
		return () => {
			cancel = true; // fix 2. we don't care about the data, prevent the race condition
			controller.abort(); // fix 3. actually abort network download
		}

	},[props.customerId]); // fix 4. make sure we have the customerId here so that we reload the visits when it changes!
}

Don't over-react

As with all frameworks and libraries, React is a tool. Whether it is useful, and how it should be used depends on your situation.

The above is just our limited experience, it might very well not fit your needs.

However, we feel that simple is better, and React can dramatically simplify your life, if you go with the React idea - pure rendering or pure data.

That's all, don't give in to the dogma, and happy hacking!

Continue reading

Travel Log Update

Case management app

January 17, 2025
Case management app

Case-workers such as social workers or health workers, divide their time between clients. The purpose of our app is to support them in their work. And to make the inevitable paperwork - reporting as simple as possible.

Real estate Lease management

January 05, 2025
Real estate Lease management

What is missing in a housing market with demand for rental properties and many owners with a single extra property to rent?
Services. Keeping the property occupied, choosing the right tenants, ongoing maintenance, inspections and all the legal paperwork.

©2022-2025 Inuko s.r.o
Privacy PolicyCloud
ENSKCZ
ICO 54507006
VAT SK2121695400
Vajanskeho 5
Senec 90301
Slovakia EU

contact@inuko.net