How does JavaScript mechanism behind react hooks work? - javascript

My question relates to Javascript mechanisms that make react hooks possible.
Recent development in React allows us to create hooks, ie. for React state, within as simple function like:
function App () {
const [someVar, setSomeVar] = useState('someVarDefaultValue');
return (
<div
onClick={() => setSomeVar('newValue')}>{someVar}
</div>
);
}
The hook useState returns an array with an accessor and a mutator, and we use them by array decomposition inside our App function.
So under the hood, the hook looks something like (just a pseudocode):
function useState(defaultValue) {
let value = defaultValue;
function setValue(val) {
value = val;
}
return [value, setValue];
}
When you try this approach in JS it won't work - value decomposed from array will not update if you use setValue somewhere. Even if you use the value as an object, not a primitive defaultValue.
My question is how does hook mechanism work in JS?
From what I've seen in React sourcecode it uses reducer function and type-checking with Flow. The code is tricky to follow for me to understand the big picture.
This question is not about how to write custom hooks in React.
It's also not question how hooks work under the hood in context of React state management answered in this question: React Hooks - What's happening under the hood?

The state value has to be stored outside of the useState function, in some internal representation of the component instance, so that it returns persistent results across calls. Additionally setting the value has to cause a rerender on the component it gets called in:
// useState must have a reference to the component it was called in:
let context;
function useState(defaultValue) {
// Calling useState outside of a component won't work as it needs the context:
if (!context) {
throw new Error("Can only be called inside render");
}
// Only initialize the context if it wasn't rendered yet (otherwise it would re set the value on a rerender)
if (!context.value) {
context.value = defaultValue;
}
// Memoize the context to be accessed in setValue
let memoizedContext = context;
function setValue(val) {
memoizedContext.value = val;
// Rerender, so that calling useState will return the new value
internalRender(memoizedContext);
}
return [context.value, setValue];
}
// A very simplified React mounting logic:
function internalRender(component) {
context = component;
component.render();
context = null;
}
// A very simplified component
var component = {
render() {
const [value, update] = useState("it");
console.log(value);
setTimeout(update, 1000, "works!");
}
};
internalRender(component);
Then when setValue gets called, the component rerenders, useState will get called again, and the new value will get returned.
The upper example is very simplified. Here's a few things that React does differently:
The state is not stored in a "context property" but rather in a linked list. Whenever useState is called, the linked list advances to the next node. That's why you should not use hooks in branches/loops.
The setState function gets cached and the same reference gets returned each time.
Rerendering does not happen synchronously.

In the following, multiple calls of useState() simulated by an array for each state variable.
In each state updater method call, render will be called by React.
Thus, we force render by calling an original state updater method(i.e. setValue) after calling our simulated state updater.
Function Component (SimpleForm) will be called for rendering so React will reset the context(not state) for component internally before invoking this method.
Thus, we simulate this with a resetContext method.
import React, { useState } from "react";
import ReactDOM from "react-dom";
import "./styles.css";
let myState = {};
let counter = 0;
function resetContext() {
counter = 0;
}
function myUseState(initialValue) {
console.log("counter: ", counter, " - myState:", myState);
const notAlreadyDefined = myState[counter] === undefined;
if (notAlreadyDefined) {
myState[counter] = initialValue;
}
let cnt = counter;
const pair = [
myState[cnt],
(val) => {
console.log("setter", val, cnt);
myState[cnt] = val;
// In each updater method, render() will be called by React.
// So, we force render by calling an original state updater method(i.e. setValue) after calling our simulated state updater.
}
];
counter++;
return pair;
}
function SimpleForm(props) {
const [value, setValue] = useState("John");
const [value2, setValue2] = useState("Edward");
// Function Component (SimpleForm) will be called to render so React will reset the context(not state) for component internally before invoking this method.
// So, we simulate this with a resetContext method.
resetContext();
const [firstName, setFirstName] = myUseState("John");
const [lastName, setLastName] = myUseState("Edward");
const [age, setAge] = useState(30);
console.log("called", new Date(), firstName, lastName);
return (
<form>
<label>
First Name:
<input
type="text"
value={firstName}
onChange={(event) => {
setValue(event.target.value);
setFirstName(event.target.value);
}}
/>
</label>
<br />
<label>
Last Name:
<input
type="text"
value={lastName}
onChange={(event) => {
setValue2(event.target.value);
setLastName(event.target.value);
}}
/>
</label>
<br />
<label>
Age:
<input
type="number"
value={age}
onChange={(event) => setAge(event.target.value)}
/>
</label>
<br />
<input type="submit" value="Submit" />
</form>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(
<>
<SimpleForm firstName="JOHN" lastName="Edward" age={30} />
<br />
</>,
rootElement
);
https://codesandbox.io/s/react-usestate-hook-example-forked-unjk1m?file=/src/index.js
Also, check the explanation from React Docs
Under the Hood How does React associate Hook calls with 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.
https://reactjs.org/docs/hooks-faq.html#under-the-hood

Related

Is there any way to trigger React.useEffect from different component?

Imagine two components like this in React:
import MyComponent2 from "./components/MyComponent2";
import React from "react";
export default function App() {
const [myState, setMyState] = React.useState([]);
React.useEffect(() => {
console.log("useEffect triggered");
}, [myState]);
return <MyComponent2 myState={myState} setMyState={setMyState} />;
}
import React from "react";
export default function MyComponent2(props) {
const [inputValue, setInputValue] = React.useState("");
function handleChange(e) {
setInputValue(e.target.value);
let list = props.myState;
list.push(`${e.target.value}`);
props.setMyState(list);
console.log(props.myState);
}
return (
<div>
<input
type="text"
value={inputValue}
name="text"
onChange={handleChange}
/>
</div>
);
}
As you can see I am making changes with props.setMyState line in second component. State is changing but Somehow I could not trigger React.useEffect in first component even tough It is connected with [myState]. Why ?
In short form of my question : I can not get "useEffect triggered" on my console when i make changes in input
Instead of providing myState and setMyState to MyComponent2, you should only provide setMyState and use the functional update argument in order to access the current state.
In your handleChange function you are currently mutating the React state (modifying it directly):
let list = props.myState; // This is an array that is state managed by React
list.push(`${e.target.value}`); // Here, you mutate it by appending a new element
props.setMyState(list);
// ^ You update the state with the same array here,
// and since they have the same object identity (they are the same array),
// no update occurs in the parent component
Instead, you should set the state to a new array (whose object identity differs from the current array):
props.setMyState(list => {
const newList = [...list];
newList.push(e.target.value);
return newList;
});
// A concise way to write the above is like this:
// props.setMyState(list => [...list, e.target.value]);

How does prevState in new useState in React work ?? How does it store the value of previous state?

I know how to use prevState in new State, I want to know how does it actually works ?? How does this functionality works ?? why on putting in 'prevValue' in 'setFullName' function changes its value ?
import React, { useState } from "react";
function App() {
const [fullName, setFullName] = useState({
firstName: "",
lastName: ""
});
function handleChange(event) {
let value = event.target.value;
let name = event.target.name;
setFullName((prevValue) => {
if (name === "fName") {
return {
firstName: value,
lastName: prevValue.lastName
};
} else if (name === "lName") {
return {
firstName: prevValue.firstName,
lastName: value
};
}
});
}
return (
<div className="container">
<h1>
{fullName.firstName} {fullName.lastName}
</h1>
<form>
<input onChange={handleChange} name="fName" placeholder="First Name" />
<input onChange={handleChange} name="lName" placeholder="Last Name" />
<button>Submit</button>
</form>
</div>
);
}
export default App;
When a component is created (with React.createElement, or with JSX like <App />), React will see how many times useState is called in the component body, and will create an internal mapping of state indicies to state values. For example, with the following component:
const Comp = () => {
const [v1, setV1] = useState('a');
const [v2, setV12] = useState('b');
return <div>Text</div>;
};
When called, React will, internally, now have something like the following:
[ // array of all stateful values for this rendered component
'a', // corresponds to the stateful value of the first useState
'b', // corresponds to the stateful value of the second useState
]
These internal state values are changed when the state setter is called (eg setV1), and then when the component gets re-rendered, the new stateful values (coming from React internals) are then returned by useState.
When the state setter is passed a callback, eg, with your
setFullName((prevValue) => {
All that's really needed for this to work is for React to pass the current corresponding state value in React's internals as prevValue.
When state is set, the corresponding value in React's internals gets updated, but there may be other synchronous code that runs before the component gets re-rendered, and the new values (coming from React internals) get returned by the next invocations of useState. That's why using the callback form of the state setter sometimes gives different results - the callback argument always comes from React's internals, but the state value returned by useState only refers to the state at the time the component was rendered.
For example
const [count, setCount] = useState(0);
const clickHandler = () => {
setCount(count + 1); // sets the state in React's internals to 1
setCount(count + 1); // sets the state in React's internals to 1, not 2;
// because `count` still refers to the initial value of 0
};
const [count, setCount] = useState(0);
const clickHandler = () => {
setCount(count + 1); // sets the state in React's internals to 1
setCount(prevCount => prevCount + 1); // sets the state in React's internals to 2
// because `prevCount` comes directly from React's internals
};
But, for your code here, because multiple state changes are not occurring in the same render, there's no need to use the callback form - feel free to use the outside fullName identifier instead of using the prevValue and a callback.

Prevent infinite loop on function component with default parameter and useEffect

When creating a function component in React and setting a default parameter everything works like expected and the component will be rendered once. But as soon as you add a hook like useEffect and use this parameter in the dependency array the component rerenders forever.
I've created a simple demo here: https://codesandbox.io/s/infinite-useeffect-loop-on-default-value-tv7hj?file=/src/TestComponent.jsx
The reason is quite obvious, because when using an object as default parameter, it will be created again and will not be equal to the previous one. And of course this doesn't happen on primitive default parameter values like number or string.
Is there any better way to avoid this side effect besides using defaultProps?
Yes, instead of setting the default value of value to being an object, just set it to false. Then check if value is truthy, if it is, then access the correct properties, otherwise, just show a default value. New code.
It would be something like:
import { useEffect, useState } from "react";
const TestComponent = ({ value = false }) => {
const [calcValue, setCalcValue] = useState(0);
useEffect(() => {
setCalcValue((cur) => cur + 1);
}, [value]);
return (
<div>
{value ? value.name : "Test"}:{calcValue}
</div>
);
};
The reason you get infinite loops is because the reference of value keeps changing.
The first time the component is rendered, it sees a new reference to value, which triggers the useEffect, which in turns modifies the state of the component, and this leads to a new render, which causes value to be re-created once again because the old reference to that variable has changed.
The easiest way to deal with this is to just create a default value outside the component and use that (basically the same as the defaultProps solution):
import { useEffect, useState } from "react";
const defaultValue = {name: "Test"}; // <-- default here
const TestComponent = ({ value = defaultValue }) => {
const [calcValue, setCalcValue] = useState(0);
useEffect(() => {
setCalcValue((cur) => cur + 1);
}, [value]);
return (
<div>
{value.name}:{calcValue}
</div>
);
};
Doing this will ensure that each time the component renders, it sees the same reference for value, therefore the useEffect hook only runs once.
Another way of dealing with this is to first wrap your component with memo, then create a new state variable which takes on the original value, and make your useEffect hook depend on this new state variable:
const TestComponent = React.memo(({ value = {name: "Test"} }) => {
const [calcValue, setCalcValue] = useState(0);
const [myValue, setMyValue] = useState(value);
useEffect(() => {
setCalcValue((cur) => cur + 1);
}, [myValue]);
return (
<div>
{myValue.name}:{calcValue}
</div>
);
});
The reason why we wrap the component with memo is so that it only re-renders after a state change if the prop had changed in value (instead of reference). You can change the way memo detects props changes by providing a custom comparison function as a second parameter.

Use dynamically created react components and fill with state values

Below is a proof of concept pen. I'm trying to show a lot of input fields and try to collect their inputs when they change in one big object. As you can see, the input's won't change their value, which is what I expect, since they're created once with the useEffect() and filled that in that instance.
I think that the only way to solve this is to use React.cloneElement when values change and inject the new value into a cloned element. This is why I created 2000 elements in this pen, it would be a major performance hog because every element is rerendered when the state changes. I tried to use React.memo to only make the inputs with the changed value rerender, but I think cloneElement simply rerenders it anyways, which sounds like it should since it's cloned.
How can I achieve a performant update for a single field in this setup?
https://codepen.io/10uur/pen/LYPrZdg
Edit: a working pen with the cloneElement solution that I mentioned before, the noticeable performance problems and that all inputs rerender.
https://codepen.io/10uur/pen/OJLEJqM
Here is one way to achieve the desired behavior :
https://codesandbox.io/s/elastic-glade-73ivx
Some tips :
I would not recommend putting React elements in the state, prefer putting plain data (array, objects, ...) in the state that will be mapped to React elements in the return/render method.
Don't forget to use a key prop when rendering an array of elements
Use React.memo to avoid re-rendering components when the props are the same
Use React.useCallback to memoize callback (this will help when using React.memo on children)
Use the functional form of the state setter to access the old state and update it (this also helps when using React.useCallback and avoid recreating the callback when the state change)
Here is the complete code :
import React, { useEffect } from "react";
import ReactDOM from "react-dom";
import "./styles.css";
const INPUTS_COUNT = 2000;
const getInitialState = () => {
const state = [];
for (var i = 0; i < INPUTS_COUNT; i++) {
// Only put plain data in the state
state.push({
value: Math.random(),
id: "valueContainer" + i
});
}
return state;
};
const Root = () => {
const [state, setState] = React.useState([]);
useEffect(() => {
setState(getInitialState());
}, []);
// Use React.useCallback to memoize the onChangeValue callback, notice the empty array as second parameter
const onChangeValue = React.useCallback((id, value) => {
// Use the functional form of the state setter, to update the old state
// if we don't use the functional form, we will be forced to put [state] in the second parameter of React.useCallback
// in that case React.useCallback will not be very useful, because it will recreate the callback whenever the state changes
setState(state => {
return state.map(item => {
if (item.id === id) {
return { ...item, value };
}
return item;
});
});
}, []);
return (
<>
{state.map(({ id, value }) => {
// Use a key for performance boost
return (
<ValueContainer
id={id}
key={id}
onChangeValue={onChangeValue}
value={value}
/>
);
})}
</>
);
};
// Use React.memo to avoid re-rendering the component when the props are the same
const ValueContainer = React.memo(({ id, onChangeValue, value }) => {
const onChange = e => {
onChangeValue(id, e.target.value);
};
return (
<>
<br />
Rerendered: {Math.random()}
<br />
<input type="text" value={value} onChange={onChange} />
<br />
</>
);
});
ReactDOM.render(<Root />, document.getElementById("root"));

How to force a functional React component to render?

I have a function component, and I want to force it to re-render.
How can I do so?
Since there's no instance this, I cannot call this.forceUpdate().
🎉 You can now, using React hooks
Using react hooks, you can now call useState() in your function component.
useState() will return an array of 2 things:
A value, representing the current state.
Its setter. Use it to update the value.
Updating the value by its setter will force your function component to re-render,
just like forceUpdate does:
import React, { useState } from 'react';
//create your forceUpdate hook
function useForceUpdate(){
const [value, setValue] = useState(0); // integer state
return () => setValue(value => value + 1); // update state to force render
// A function that increment 👆🏻 the previous state like here
// is better than directly setting `setValue(value + 1)`
}
function MyComponent() {
// call your hook here
const forceUpdate = useForceUpdate();
return (
<div>
{/*Clicking on the button will force to re-render like force update does */}
<button onClick={forceUpdate}>
Click to re-render
</button>
</div>
);
}
You can find a demo here.
The component above uses a custom hook function (useForceUpdate) which uses the react state hook useState. It increments the component's state's value and thus tells React to re-render the component.
EDIT
In an old version of this answer, the snippet used a boolean value, and toggled it in forceUpdate(). Now that I've edited my answer, the snippet use a number rather than a boolean.
Why ? (you would ask me)
Because once it happened to me that my forceUpdate() was called twice subsequently from 2 different events, and thus it was reseting the boolean value at its original state, and the component never rendered.
This is because in the useState's setter (setValue here), React compare the previous state with the new one, and render only if the state is different.
Update react v16.8 (16 Feb 2019 realease)
Since react 16.8 released with hooks, function components have the ability to hold persistent state. With that ability you can now mimic a forceUpdate:
function App() {
const [, updateState] = React.useState();
const forceUpdate = React.useCallback(() => updateState({}), []);
console.log("render");
return (
<div>
<button onClick={forceUpdate}>Force Render</button>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.1/umd/react-dom.production.min.js"></script>
<div id="root"/>
Note that this approach should be re-considered and in most cases when you need to force an update you probably doing something wrong.
Before react 16.8.0
No you can't, State-Less function components are just normal functions that returns jsx, you don't have any access to the React life cycle methods as you are not extending from the React.Component.
Think of function-component as the render method part of the class components.
Official FAQ now recommends this way if you really need to do it:
const [ignored, forceUpdate] = useReducer(x => x + 1, 0);
function handleClick() {
forceUpdate();
}
Simplest way 👌
if you want to force a re-render, add a dummy state you can change to initiate a re-render.
const [rerender, setRerender] = useState(false);
...
setRerender(!rerender); //whenever you want to re-render
And this will ensure a re-render, And you can call setRerender(!rerender) anywhere, whenever you want :)
I used a third party library called
use-force-update
to force render my react functional components. Worked like charm.
Just use import the package in your project and use like this.
import useForceUpdate from 'use-force-update';
const MyButton = () => {
const forceUpdate = useForceUpdate();
const handleClick = () => {
alert('I will re-render now.');
forceUpdate();
};
return <button onClick={handleClick} />;
};
Best approach - no excess variables re-created on each render:
const forceUpdateReducer = (i) => i + 1
export const useForceUpdate = () => {
const [, forceUpdate] = useReducer(forceUpdateReducer, 0)
return forceUpdate
}
Usage:
const forceUpdate = useForceUpdate()
forceUpdate()
If you already have a state inside the function component and you don't want to alter it and requires a re-render you could fake a state update which will, in turn, re-render the component
const [items,setItems] = useState({
name:'Your Name',
status: 'Idle'
})
const reRender = () =>{
setItems((state) => [...state])
}
this will keep the state as it was and will make react into thinking the state has been updated
This can be done without explicitly using hooks provided you add a prop to your component and a state to the stateless component's parent component:
const ParentComponent = props => {
const [updateNow, setUpdateNow] = useState(true)
const updateFunc = () => {
setUpdateNow(!updateNow)
}
const MyComponent = props => {
return (<div> .... </div>)
}
const MyButtonComponent = props => {
return (<div> <input type="button" onClick={props.updateFunc} />.... </div>)
}
return (
<div>
<MyComponent updateMe={updateNow} />
<MyButtonComponent updateFunc={updateFunc}/>
</div>
)
}
The accepted answer is good.
Just to make it easier to understand.
Example component:
export default function MyComponent(props) {
const [updateView, setUpdateView] = useState(0);
return (
<>
<span style={{ display: "none" }}>{updateView}</span>
</>
);
}
To force re-rendering call the code below:
setUpdateView((updateView) => ++updateView);
None of these gave me a satisfactory answer so in the end I got what I wanted with the key prop, useRef and some random id generator like shortid.
Basically, I wanted some chat application to play itself out the first time someone opens the app. So, I needed full control over when and what the answers are updated with the ease of async await.
Example code:
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
// ... your JSX functional component, import shortid somewhere
const [render, rerender] = useState(shortid.generate())
const messageList = useRef([
new Message({id: 1, message: "Hi, let's get started!"})
])
useEffect(()=>{
async function _ () {
await sleep(500)
messageList.current.push(new Message({id: 1, message: "What's your name?"}))
// ... more stuff
// now trigger the update
rerender(shortid.generate())
}
_()
}, [])
// only the component with the right render key will update itself, the others will stay as is and won't rerender.
return <div key={render}>{messageList.current}</div>
In fact this also allowed me to roll something like a chat message with a rolling .
const waitChat = async (ms) => {
let text = "."
for (let i = 0; i < ms; i += 200) {
if (messageList.current[messageList.current.length - 1].id === 100) {
messageList.current = messageList.current.filter(({id}) => id !== 100)
}
messageList.current.push(new Message({
id: 100,
message: text
}))
if (text.length === 3) {
text = "."
} else {
text += "."
}
rerender(shortid.generate())
await sleep(200)
}
if (messageList.current[messageList.current.length - 1].id === 100) {
messageList.current = messageList.current.filter(({id}) => id !== 100)
}
}
If you are using functional components with version < 16.8. One workaround would be to directly call the same function like
import React from 'react';
function MyComponent() {
const forceUpdate = MyComponent();
return (
<div>
<button onClick={forceUpdate}>
Click to re-render
</button>
</div>
);
}
But this will break if you were passing some prop to it. In my case i just passed the same props which I received to rerender function.
For me just updating the state didn't work. I am using a library with components and it looks like I can't force the component to update.
My approach is extending the ones above with conditional rendering. In my case, I want to resize my component when a value is changed.
//hook to force updating the component on specific change
const useUpdateOnChange = (change: unknown): boolean => {
const [update, setUpdate] = useState(false);
useEffect(() => {
setUpdate(!update);
}, [change]);
useEffect(() => {
if (!update) setUpdate(true);
}, [update]);
return update;
};
const MyComponent = () => {
const [myState, setMyState] = useState();
const update = useUpdateOnChange(myState);
...
return (
<div>
... ...
{update && <LibraryComponent />}
</div>
);
};
You need to pass the value you want to track for change. The hook returns boolean which should be used for conditional rendering.
When the change value triggers the useEffect update goes to false which hides the component. After that the second useEffect is triggered and update goes true which makes the component visible again and this results in updating (resizing in my case).

Categories

Resources