React Hooks - What's happening under the hood? - javascript

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?

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.

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?

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 article does not explain in technical terms how React schedule calls for later phases, but rather demonstrates what are the rules that react uses to schedule calls for later phases.

The URL in another answer given by Eliav Louski is so far the best React explaination I have come across. This page should replace React's official tutorial as it removes all the magic from hooks and friends.

Related

Is it safe to use hooks in createSelector?

I have just found out that I can use data hooks in createSelector functions and it works. An example:
// This is a normal hook
const useUserReducer = () => {
const userAccessData = useSelector(state => state?.userAccessData)
return userAccessData
}
// Here I use the hook as first argument!
export const useUserReducerFromCreateSelector = createSelector(useUserReducer, (result) => {
console.log(result) // userAccessData printed correctly
return result
})
Then I use it in my component as a normal hook:
const Component = () => {
const result = useUserReducerFromCreateSelector([])
console.log(result) // userAccessData printed correctly
return (
<>
{JSON.stringify(result)}
</>
)
}
I dont see any documentation about this, so I wonder if its safe to use it. It would help me a lot creating reusable selectors.
(I tested while changing the state at various points in time and I always see the correct state)
It is certainly an abuse if it is working. createSelector is only supposed to be a pure state selector function, so naming the returned selector function like a React hook, i.e. useUserReducerFromCreateSelector, is likely to cause some linter warnings eventually.
The potential issue is that Reselect and createSelector creates memoized selector functions. If the input value to a selector doesn't change, then the selector function returns the previously computed selector value. This means that a selector using a React hook like this potentially conditionally calling a React hook which is a violation of the Rules of Hooks.
Only Call Hooks at the Top Level
Don’t call Hooks inside loops, conditions, or nested functions. Instead, always use Hooks at the top level of your React function,
before any early returns. By following this rule, you ensure that
Hooks are called in the same order each time a component renders.
That’s what allows React to correctly preserve the state of Hooks
between multiple useState and useEffect calls. (If you’re curious,
we’ll explain this in depth below.)
Only Call Hooks from React Functions
Don’t call Hooks from regular JavaScript functions. Instead, you
can:
✅ Call Hooks from React function components.
✅ Call Hooks from custom Hooks (we’ll learn about them on the next page).
By following this rule, you ensure that all stateful logic in a
component is clearly visible from its source code.
I don't consider it safe to use any React hook in a selector function like this.
Split out the logic of selecting the state from the useUserReducerFromCreateSelector hook to be used in your selector functions.
Example:
const userAccessData = state => state?.userAccessData || {};
const computedUserAccessData = createSelector(
[userAccessData],
data => {
// logic to compute derived state, etc...
return newUserAccessData;
},
);
I was really intrigued seeing this particular use in the redux-toolkit github repo issues and nobody complaining about it so I decided to ask the same question in the reselect github page.
Here is the response of Mark Erikson (redux maintainer):
No, this is not safe!
You're technically getting away with it because of how you're using
that in a component. But if you were to try to use that selector
outside of a component, it would break.
I'd really recommend sticking with keeping these concepts separate.
Write and name selectors as selectors. Write and name hooks as hooks.
Don't try and mix the two :)
To be clear, the code that you wrote above should run. It's ultimately
"just" composition of functions and calling them in a particular
order.
But given how hooks work, and how selectors work, it's best to keep
those concepts separate when writing the code to avoid confusion.

React useReducer Hook fires twice / how to pass props to reducer?

FOREWORD / DESCRIPTION
I am trying to use React's new hooks feature for an e-commerce website that I am building, and have been having an issue working a bug out of my shopping cart component.
I think it is relevant to preface the discussion with the fact that I am trying to keep my global state modular by using multiple Context components. I have a separate context component for the types of items that I offer, and a separate context component for the items in a person's shopping cart.
PROBLEM
The issue I am having is that when I dispatch an action to add a component to my cart, the reducer will run twice as if I had added the item to my cart twice. But only when it is initially rendered, or for weird reasons such as the display is set to hidden and then back to block or for a change in the z-index and potentially other similar changes.
I know this is kind of verbose, but it is rather knit picky issue so I have created two codepens that showcase the issue:
full example
minimum example
You will see that I have included a button to toggle the display of the components. This will help showcase the correlation of the css to the issue.
Finally please monitor the console in the code pens, this will show all button clicks and which part of each reducer has been run. The issues are most evident in the full example, but the console statements display the issue is also present in the minimum example.
PROBLEM AREA
I have pinpointed the problem to be related to the fact that I am using the state of a useContext hook to get the items list. A function is called to generate the reducer for my useReducer hook, but only arises when a different hook is used AKA I could use a function that wouldn't be subject to re-eval like hook is and not have the issue, but I also need the info from my previous Context so that workaround doesn't really fix my issue.
Relevant Links
I have determined the issue is NOT an HTML issue so I will not include the links to the HTML fixes I have tried. The issue, while triggered by css, is not rooted in css so I will not include css links either.
useReducer Action dispatched twice
As you indicated, the cause is the same as the related answer of mine that you linked to. You are re-creating your reducer whenever Provider is re-rendered, so in some cases React will execute the reducer in order to determine whether or not it needs to re-render Provider and if it does need to re-render it will detect that the reducer is changed, so React needs to execute the new reducer and use the new state produced by it rather than what was returned by the previous version of the reducer.
When you can't just move the reducer out of your function component due to dependencies on props or context or other state, the solution is to memoize your reducer using useCallback, so that you only create a new reducer when its dependencies change (e.g. productsList in your case).
The other thing to keep in mind is that you shouldn't worry too much about your reducer executing twice for a single dispatch. The assumption React is making is that reducers are generally going to be fast enough (they can't do anything with side effects, make API calls, etc.) that it is worth the risk of needing to re-execute them in certain scenarios in order to try to avoid unnecessary re-renders (which could be much more expensive than the reducer if there is a large element hierarchy underneath the element with the reducer).
Here's a modified version of Provider using useCallback:
const Context = React.createContext();
const Provider = props => {
const memoizedReducer = React.useCallback(createReducer(productsList), [productsList])
const [state, dispatch] = React.useReducer(memoizedReducer, []);
return (
<Context.Provider value={{ state, dispatch }}>
{props.children}
</Context.Provider>
);
}
Here is a modified version of your codepen: https://codepen.io/anon/pen/xBdVMp?editors=0011
Here are a couple answers related to useCallback that might be helpful if you aren't familiar with how to use this hook:
Trouble with simple example of React Hooks useCallback
React Hooks useCallback causes child to re-render
Seperate the Reducer from the functional component that helped me solve mine
An example based on Ryans excellent answer.
const memoizedReducer = React.useCallback((state, action) => {
switch (action.type) {
case "addRow":
return [...state, 1];
case "deleteRow":
return [];
default:
throw new Error();
}
}, []) // <--- if you have vars/deps inside the reducer that changes, they need to go here
const [data, dispatch] = React.useReducer(memoizedReducer, _data);
When I read some useContext source code, i found
const useContext = hook(class extends Hook {
call() {
if(!this._ranEffect) {
this._ranEffect = true;
if(this._unsubscribe) this._unsubscribe();
this._subscribe(this.Context);
this.el.update();
}
}
After the first time update, a effect like is called after the update. After the value is subscribed to the right context, for instance, resolving the value from Provider, it requests another update. This is not a loop, thanks to _ranEffect flag.
Seems to me if above is true for React, the render engine are called twice.

Update react context outside of a consumer?

I am trying to understand how the new react context API works.
In redux, it is possible for a component to have knowledge of dispatch actions without knowing state. This allows updates to redux state without causing a rerender of components that don't care about that state.
For example I could have
<Updater onClick={updateCount}/>
and
<Consumer value={count}/>
Updater is connected to dispatch(updateCount()) and Consumer is connected to count's current value via state.count. When state.count is updated, only the Consumer rerenders. To me, that's a crucial behavior.
In react context, it seems very difficult to duplicate this behavior. I'd like to be able to update state without causing unnecessary rerenders of components that want to alter the context but don't actually care about the state.
How would it be possible for components to trigger updates to context if they are not inside a consumer? And I definitely don't want to trigger an update to the entire tree by setting state at the provider level.
interesting question. Not sure you can without at least an extra layer (but happy to be shown wrong).
Maybe using Memo or PureComponent to minimise the re-rendering?
import React, { memo } from 'react';
function Widget({ setContext }) {
return <button onClick={setContext}/>Click Me</button>;
}
export default memo(Widget);
...
function Wrap() {
const { setSession } = useContext(SessionContext);
return <Widget setSession={setSession} />;
}
One possible solution is to transform your consumer components into pure components and check against the values each component really cares about.
This can be easily done using the onlyUpdateForKeys HOC from recompose.
you can try this library react-hooks-in-callback to isolate the context from your component and pick only desired state values from it,
check this example

How does useState() in react retrieve the correct state object and function for a functional component when using the state hook? [duplicate]

This question already has an answer here:
How does React implement hooks so that they rely on call order
(1 answer)
Closed 3 years ago.
I am currently working on my understanding of the useState hook from react.
What I would like to know is; when useState is called how is it able to correctly retrieve the state object and the function that can be used to modify it, for that specific functional component (assuming it has already been created the first time it was called.)
Another way to phrase my question would be where does count and setCount exist? useState() will obviously return a different state object and modifier function depending on which functional component useState is called in, so how does that work?
My guess would be that a closure is formed which means that this functional component has a lexical environment which consists of any local variables that were in-scope at the time the closure was created and this is what makes count and setCount available when useState is called. But I haven't been able to confirm this and since react is involved I could be way off.
export const MyFunctionalComponent = () => {
const [count, setCount] = useState(0);
return (
<h1>This is my component.</h1>
);
}
Can anyone clear this up for me?
#edit: I'm pretty sure I'm way off with my thinking about closures, looking at the react library source code I found the implementation for the useState function.
export function useState<S>(initialState: (() => S) | S) {
const dispatcher = resolveDispatcher();
return dispatcher.useState(initialState);
}
I'm probably going to have to dig in and unpack this to get an answer.
I found the following on the React documentation page which at least gives a basic description on how useState is able to get back local state for functional components.
React keeps track of the currently rendering component. Thanks to the Rules of Hooks, we know that Hooks are only called from React
components (or custom Hooks — which are also only called from React
components).
There is an internal list of “memory cells” associated with each
component. They’re just JavaScript objects where we can put some data.
When you call a Hook like useState(), it reads the current cell (or
initializes it during the first render), and then moves the pointer to
the next one. This is how multiple useState() calls each get
independent local state.

Best pattern to rerender my react component?

I have a global data object I update and then I call React.renderComponent() again on the top/main component.
Is this the right pattern for triggering this update?
You should generally pass the data object into the component as a prop, even if it's a global variable. This lets you test the component, and also use it elsewhere without being tied to that global.
As Mike said, there's nothing wrong with using React.renderComponent to update it.
He also mentioned flux, but that's overkill for this. A simple event emitter where you do something like .emit('change', newData), and the component is listening for the change event tends to be better for simpler cases. See my answer to this question for an example of how that can be done.
This is the correct pattern. React.renderComponent will either mount a component for the first time, or get an already mounted component to update.
If you're using a global object though, you might want to look in to the Flux architecture here:
http://facebook.github.io/flux/docs/overview.html
I had the same problem and asked myself if I really needed to re-render the component.
You can do so with this.forceUpdate() but it's not advisable. As React docs states:
You should try to avoid all uses of forceUpdate() and only read from this.props and this.state in render(). This makes your component "pure" and your application much simpler and more efficient.
So what I did was create a data property like exists and test it:
// renderDeleteButton() is being called on render()
renderDeleteButton () {
if (!this.props.store.exists) {
return;
}
return(
<DeleteButton
...
deleteAction={this.delete} />
);
}
Whenever I delete/save, I toggle exists and component will show up or hide based on that. React handles that for me.

Categories

Resources