50
votes

I've been trying out React Hooks and they do seem to simplify things like storing state. However, they seem to do a lot of things by magic and I can't find a good article about how they actually work.

The first thing that seems to be magic is how calling a function like useState() causes a re-render of your functional component each time you call the setXXX method it returns?

How does something like useEffect() fake a componentDidMount when functional components don't even have the ability to run code on Mount/Unmount?

How does useContext() actually get access to the context and how does it even know which component is calling it?

And that doesn't even begin to cover all of the 3rd party hooks that are already springing up like useDataLoader which allows you to use the following...

const { data, error, loading, retry } = useDataLoader(getData, id)

How do data, error, loading and retry re-render your component when they change?

Sorry, lots of questions but I guess most of them can be summed up in one question, which is:

How does the function behind the hook actually get access to the functional/stateless component that is calling it so that it can remember things between re-renders and initiate a re-render with new data?

3

3 Answers

41
votes

React hook makes use of hidden state of a component, it's stored inside a fiber, a fiber is an entity that corresponds to component instance (in a broader sense, because functional components don't create instances as class components).

It's React renderer that gives a hook the access to respective context, state, etc. and incidentally, it's React renderer that calls component function. So it can associate component instance with hook functions that are called inside of component function.

This snippet explains how it works:

let currentlyRenderedCompInstance;
const compStates = new Map(); // maps component instances to their states
const compInstances = new Map(); // maps component functions to instances

function useState(initialState) {
  if (!compStates.has(currentlyRenderedCompInstance))
    compStates.set(currentlyRenderedCompInstance, initialState);

  return [
    compStates.get(currentlyRenderedCompInstance) // state
    val => compStates.set(currentlyRenderedCompInstance, val) // state setter
  ];
}

function render(comp, props) {
  const compInstanceToken = Symbol('Renderer token for ' + comp.name);

  if (!compInstances.has(comp))
    compInstances.set(comp, new Set());

  compInstances.get(comp).add(compInstanceToken);

  currentlyRenderedCompInstance = compInstanceToken;

  return { 
    instance: compInstanceToken,
    children: comp(props)
  };
}

Similarly to how useState can access currently rendered component instance token through currentlyRenderedCompInstance, other built-in hooks can do this as well and maintain state for this component instance.

11
votes

Dan Abramov created a blog post just a couple days ago that covers this:

https://overreacted.io/how-does-setstate-know-what-to-do/

The second half specifically goes into details regarding hooks like useState.

For those interested in a deep dive into some of the implementation details, I have a related answer here: How do react hooks determine the component that they are for?

0
votes

I would recommend reading https://eliav2.github.io/how-react-hooks-work/

It includes detailed explanations about what is going on when using react hooks and demonstrate it with many interactive examples.

Note - the does not explain how React schduale calls for later phases, but deeply the demonstrate in what are the rules that react uses to schedule calls for later phases