How does React Hooks re-renders a function Component?

⚠️ This post is not on how to use React hooks. There are plenty of great resources about it. In this post, I will try to explain my comprehension about the mechanism behind React hooks’ functionality, how it might work behind the scene, how it might maintain the state for a function component, how it re-renders a function Component on the state update. If you are looking for resources to get started with React or React Hooks, I would highly recommend starting with the official React documentation.

Please note, I am not familiar with the actual implementation of React hooks, it might not be an exact match of the approach explained in this post, but I think it should be similar.


Photo by Bundo Kim on Unsplash

Without any further ado, let’s get started.

React Hooks was publicly announced at React Conf, on 25th October 2018. I was amazed to find out that we could maintain state for a function Component, using React hooks. Though I got hooked on to it since the very beginning, it appeared magical to me. I could not comprehend how invoking the function returned by a hook, was triggering a re-render. I tried poking at the code then, but I could not make any sense of it. So I did the best thing I could think of, I raised a StackOverflow question.

There was a reply on the post but I didn’t quite follow it. So a couple of days later, I did the next best thing I could have done. I tweeted the above mentioned StackOverflow post with the hashtag #ReactHooks.

I guess using the #ReactHooks hashtag drew Dan Abramov’s attention. He replied back stating that re-rendering mechanism is the same as the class component.

It was helpful, but I still could not comprehend how it triggers re-render. I could reason with React’s class component and how invoking this.setState would initiate a re-render.

📌 Let’s consider the following example:

const { render, Component } = OverReact;class ExtendedComponent extends Component {

constructor(props) {
super(props);
this.state = {
counter: 0,
name: 'foo'
};
}

plusOne() {
const { state: previousState} = this;
let { counter } = previousState;
counter = counter + 1;
this.setState(Object.assign(previousState, { counter }));
}
updateName(name) {
const { state: previousState} = this;
this.setState(Object.assign(previousState, { name }));
}

render() {
const { counter, name } = this.state;
console.log(`rendered, counter: ${counter}, name: ${name}`);
}
}
// initial render
render(ExtendedComponent);

⚠️ Note that we are using render and Component from OverReact and not React. We will implement OverReact, and find out how invoking this.setState re-renders a Component.
Also, note the render function of the ExtendedComponent does not return any HTML, but logs out information from the (current) state.

💻 Let’s implement the render function and Component class with the information that we are already aware of :

  • The render function accepts a Component and renders it.
  • The Component class has a constructor that sets the this.props and a setState method that triggers re-render.
// extreamly simplified implementation function render(Component) {
// ToDo
// render the Component
}
class Component {
constructor(props) {
this.props = props;
}
setState(state) {
this.state = state;
// ToDo
// triggers re-render
}
}
OverReact = {
render,
Component
}

The OverReaact.render function and the setState method of the Component class can invoke the render method of the ExtendedComponent to render it.

With the above knowledge, let's implement the ToDos.

// extreamly simplified implementationfunction render(Component) {
// create an instance of the passed Component
const instance = new Component();
// invoke the render method of the instance
instance.render();
// return the instance back
// so that we can invoke the other method of the instance
// like so:
// const instance = render(ExtendedComponent);
// instance.someInstanceMethod();
return instance;
}
class Component {
constructor(props) {
this.props = props;
}
setState(state) {
this.state = state;
// invoke the render method of the instance
this.render();
}
}
OverReact = {
render,
Component
}

Now let’s wrap this implementation in an IIFE (Immediately Invoked Function Expression) to encapsulate the render and Component implementation.

// extreamly simplified implementationOverReact = (function () {
function render(Component) {
const instance = new Component();
instance.render();
return instance;
}
class Component {
constructor(props) {
this.props = props;
}
setState(state) {
this.state = state;
this.render();
}
}
return {
render,
Component
}

}());

On stitching things together, we can observe that invoking this.setState triggers a re-render, like so:

OverReact = (function () {
function render(Component) {
const instance = new Component();
instance.render();
return instance;
}
class Component {
constructor(props) {
this.props = props;
}
setState(state) {
this.state = state;
this.render();
}
}
return {
render,
Component
}
}());
const { render, Component } = OverReact;class ExtendedComponent extends Component {

constructor(props) {
super(props);
this.state = {
counter: 0,
name: 'foo'
};
}

plusOne() {
const { state: previousState} = this;
let { counter } = previousState;
counter = counter + 1;
this.setState(Object.assign(previousState, { counter }));
}
updateName(name) {
const { state: previousState} = this;
this.setState(Object.assign(previousState, { name }));
}

render() {
const { counter, name } = this.state;
console.log(`rendered, counter: ${counter}, name: ${name}`);
}
}
// initial render
// instance is returned by the OverReact.render method
const instance = render(ExtendedComponent);
// rendered, counter: 0, name: foo
instance.plusOne();
// rendered, counter: 1, name: foo
instance.updateName('bar');
// rendered, counter: 1, name: bar
instance.plusOne();
// rendered, counter: 2, name: bar
instance.updateName('baz');
// rendered, counter: 2, name: baz
instance.plusOne();
// rendered, counter: 3, name: baz

💻 Please refer to this CodePen to see the above code in action.

I understand setState has access to the instance and thus could invoke the instance.render() to re-render the Component.
But I could not reason with how the function returned by a hook has access to the Component and triggers a re-render?
I guessed some closure to be in action but didn’t quite get it.

What is a closure?

Any variable declared in a function is only available during the execution scope of that function. However, if a function returns another function, the returned function has access to the variables declared in the outer function, even when the outer function has finished it’s execution.

Why closure?

This the only way a JavaScript function could maintain state safely, without using any persistent storage. Maintaining state in the global scope is unsafe and could lead to non-deterministic results, as anyone has access to the global scope. Thus closure seems to be the only option.

Closure 101

function greet(salutation) {
return function(noun) {
// salutation is available in this scope
// even though greet has finished its execution
console.log(`${salutation} ${noun}!`)
}
}
const hello = greet('Hello');
const hi = greet('Hi');
hello('World'); // Hello World!
hello('Universe') // Hello Universe!
hi('Venus'); // Hi Venus!
hi('Mars') // Hi Mars!

Dan shared another great post, React hooks: not magic, just arrays when I tweeted about React Hooks being magical.

React hooks: not magic, just arrays did a great job in explaining the concept but there was no concrete example.

I then came across React Hooks Demystified. It is an excellent post on how React Hook maintains the state and re-renders the component on state updates but the usage seemed to be quite different than the usage of React Hooks.

I read several other posts, where authors did a great job in explaining their comprehension about React Hooks but none of them had an implementation whose interface looked similar to React’s.

A while back I happened to watch the recording of Ryan’s React Boston presentation, where he implemented renderWithCrappyReactHooks function, to explain the concept, but renderWithCrappyReactHook was tightly coupled to a single Component. I was looking for something more decoupled and reusable implementation.

I recently came across Deep dive: How do React hooks really work?. It did a great job in explaining how React hooks maintain the state but it did not explain how invoking the function returned by a hook was triggering a re-render. The rendering in all the examples was explicit, by invoking the render function, and not as an effect of invoking the function returned by a hook.

My initial query still remained unanswered. I guess the primary reason for me not comprehending the concept was due to my passive involvement. I was looking for solutions and did not try to implement something on my own. Deep dive: How do React hooks really work? motivated me to try out something on my own.


It has been almost eight months since React Hooks was announced. I still didn’t comprehend how it works behind the scene.

A couple of days ago, I came across this StackOverflow answer by Jonas Wilms. Though the example shared in the answer is a very simplified approach, the usage implementation seems to be the closest to the actual React hooks usage. I finally built up my implementation based on it. Thank you, Jonas. 🙏

How the hook?

📌 Let’s implement the previously shared Class Component using function Component and hooks.

const { render, useState } = OverReact;function Component() {
const [counter, setCounter] = useState(0);
const [name, setName] = useState('foo');

function plusOne() {
setCounter(counter + 1);
}

function updateName(name) {
setName(name);
}
function render() {
console.log(`rendered, counter: ${counter}, name: ${name}`);
}

return {
render,
plusOne,
updateName
}
}
// initial render
render(Component);

⚠️ Note that we are using render and useState from OverReact and not React. We will reimplement OverReact, and this time find out how invoking the function returned by useState re-renders a function Component. Also, note the Component function in the above example does not return any HTML but an Object with render, plusOne and updateName functions. This simplified the implementation substantially and helped me comprehend the concept.

💻 Let’s implement the render and useState function with the information that we are already aware of :

  • The render function accepts a Component and renders it.
  • The useState function accepts a state and returns an array with the current state and a function to update the state.
  • The function returned by useState will re-render the component automatically when invoked.
// extreamly simplified implementation function render(Component) {
// ToDo
// render the Component
}
function useState(initialState) {
// ToDo
// derive currentState
let currentState;

function setState(newState) {
// ToDo
// update currentState with newState
// rerender the component
}
return [currentState, setState]
}
OverReact = {
render,
useState
}

The OverReact.render function will invoke the render method on the object returned by the Component function. We know that the setState will need to have access to the Component function, so that it can trigger a re-render by invoking the OverReact.render function passing it.

The Component function is neither passed to the useState nor to the setState function. Thus we need to store the Component function somewhere outside, so that it can be accessed by the setState function.

With the above knowledge, let’s implement the ToDos.

// extreamly simplified implementation // we will store the Component function at this context
// so it it can be accessed by setState
let context = {};
// we will use this callId to persist the state for useState call
let callId = -1;
function render(Component) {
// store the Component function to the context
context.Component = Component;
// invoke the Component function
const instance = Component();
// render the Component
instance.render();
// reset the callId after every render
callId = -1;
// add the instance to the context and retrun the context
// so that we can invoke the other method of the instance
// like so:
// const context = render(Component);
// context.instance.someInstanceMethod();
context.instance = instance;
return context;

}
function useState(initialState) {
// create a hooks array in the context if it does not exist
if (!context.hooks) {
context.hooks = [];
}


// increment the callId
// the callId will be used to store the initialState
// or retreive the existing state for that useState
callId = callId + 1;

const hooks = context.hooks;
// derive currentState
const currentState = hooks[callId] ? hooks[callId] : initialState;
// persist the current state for future use
hooks[callId] = currentState;
// closure in action
const setState = function () {
// persist the current callId
// so that it can be used to update
// the appropriate state accordingly when setState is called
const currentCallId = callId;

// return a function which will use the currentCallId
// to update the appropriate state
// and invoke the OverReact.render
// by passing the context.Component
return function (newState) {
hooks[currentCallId] = newState;
render(context.Component);
}

}();
return [currentState, setState]
}
OverReact = {
render,
useState
}

Similar to the previous OverReact implementation, let’s wrap this implementation also in an IIFE (Immediately Invoked Function Expression) to encapsulate the implementation.

// extreamly simplified implementationOverReact = (function() {  let context = {};
let callId = -1;
function render(Component) {
context.Component = Component;
const instance = Component();
instance.render();
callId = -1; context.instance = instance;
return context;
}
function useState(initialState) {
if (!context.hooks) {
context.hooks = [];
}

callId = callId + 1;

const hooks = context.hooks;
const currentState = hooks[callId] ? hooks[callId] : initialState;
hooks[callId] = currentState;
const setState = function () {
const currentCallId = callId;

return function (newState) {
hooks[currentCallId] = newState;
render(context.Component);
}
}();
return [currentState, setState]
}
return {
render,
useState
}
}());

Let’s put everything together:

OverReact = (function() {
let context = {};
let callId = -1;
function render (Component) {
context.Component = Component;
const instance = Component();
instance.render();
// reset the callId after every render
callId = -1;
// ensuring that instance.render is not available out OverReact.render
delete instance.render;
context.instance = instance;
return context;
}
function useState(initialState) {
if (!context) {
throw new Error('hooks can not be called with out a rendering context');
}
if (!context.hooks) {
context.hooks = [];
}

callId = callId + 1;

const hooks = context.hooks;
const currentState = hooks[callId] ? hooks[callId] : initialState;
hooks[callId] = currentState;
const setState = function () {
const currentCallId = callId;
return function (newState) {
hooks[currentCallId] = newState;
render(context.Component);
}
}();

return [currentState, setState];
}
return {
render,
useState
}
}());
const { render, useState } = OverReact;function Component() {
const [counter, setCounter] = useState(0);
const [name, setName] = useState('foo');

function plusOne() {
setCounter(counter + 1);
}

function updateName(name) {
setName(name);
}
function render() {
console.log(`rendered, counter: ${counter}, name: ${name}`);
}

return {
render,
plusOne,
updateName
}
}
// initial render
// context is returned by the OverReact.render method
const context = render(Component);
// rendered, counter: 0, name: foo
context.instance.plusOne();
// rendered, counter: 1, name: foo
context.instance.updateName('bar');
// rendered, counter: 1, name: bar
context.instance.plusOne();
// rendered, counter: 2, name: bar
context.instance.updateName('baz');
// rendered, counter: 2, name: baz
context.instance.plusOne();
// rendered, counter: 3, name: baz

Now we have an implementation where invoking the function returned by React Hooks triggers a re-render.

💻 Please refer to this CodePen to see the above code in action.

Conclusion

I don’t know the exact implementation of React Hooks. I tried to implement it against the React Hooks interface, by slightly modifying the interface, for simplicity. Please feel free to leave a note with your thoughts. I hope, if you have wondered, how React hooks trigger a re-render, like me, this post has been useful.

This implementation is far from perfect, in handling most of the use cases and I have no intention to reimplement React or React Hooks. React team has already done an excellent job. It is amazing to find how React team has been constantly pushing the boundaries and making it easier for developers to build, reuse, maintain modern web applications.

I am thankful to Dan, Rudi, Kayis, Ryan, Swyx, and Jonas, whose reply, blog post, presentation, answer, helped me understand the mechanism behind React hooks’ functionality.

Last but not least I am extremely thankful to Jonas Wilms, based on his StackOverflow answer I built up my solution and you for reading the post. ☕️

Medium's largest active publication, followed by +585K people. Follow to join our community.

Sarbbottam Bandyopadhyay

Written by

The Startup

More From Medium

More from The Startup

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade