Avoid unnecessary re-renders with React hook useContext - reactjs

I want to find a way to avoid unnecessary re-renders when using useContext. I've created a toy sandbox, which you can find here: https://codesandbox.io/s/eager-mclean-oupkx to demonstrate the problem. If you open the dev console, you'll see the console.log happening every second.
In this example, I have a context that simply increments a counter every 1 second. I then have a component that consumes it. I don't want every tick of the counter to cause a re-render on the component. For example, I may just want to update my component every 10 ticks, or I may want some other component that updates with every tick. I tried creating a hook to wrap useContext, and then return a static value, hoping that would prevent the re-renders, to no avail.
The best way I can think of to overcome this issue is to do something like wrap my Component in a HOC, which consumes count via useContext, and then passes the value into Component as a prop. This seems like a somewhat roundabout way of accomplishing something that should be simple.
Is there a better way to do this?

Components that call useContext directly or indirectly through custom hook will re render every time value of the provider changes.
You can call useContext from a container and let the container render a pure component or have the container call a functional component that is memoized with useMemo.
In the example below count is changed every 100 milliseconds but because useMyContext returns Math.floor(count / 10) the components will only render once every second. The containers will "render" every 100 milliseconds but they will return the same jsx 9 out of 10 times.
const {
useEffect,
useMemo,
useState,
useContext,
memo,
} = React;
const MyContext = React.createContext({ count: 0 });
function useMyContext() {
const { count } = useContext(MyContext);
return Math.floor(count / 10);
}
// simple context that increments a timer
const Context = ({ children }) => {
const [count, setCount] = useState(0);
useEffect(() => {
const i = setInterval(() => {
setCount((c) => c + 1);
}, [100]);
return () => clearInterval(i);
}, []);
const value = useMemo(() => ({ count }), [count]);
return (
<MyContext.Provider value={value}>
{children}
</MyContext.Provider>
);
};
//pure component
const MemoComponent = memo(function Component({ count }) {
console.log('in memo', count);
return <div>MemoComponent {count}</div>;
});
//functional component
function FunctionComponent({ count }) {
console.log('in function', count);
return <div>Function Component {count}</div>;
}
// container that will run every time context changes
const ComponentContainer1 = () => {
const count = useMyContext();
return <MemoComponent count={count} />;
};
// second container that will run every time context changes
const ComponentContainer2 = () => {
const count = useMyContext();
//using useMemo to not re render functional component
return useMemo(() => FunctionComponent({ count }), [
count,
]);
};
function App() {
console.log('App rendered only once');
return (
<Context>
<ComponentContainer1 />
<ComponentContainer2 />
</Context>
);
}
ReactDOM.render(<App />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>
With React-redux useSelector a component that calls useSelector will only be re rendered when the function passed to useSelector returns something different than the last time it was run and all functions passed to useSelector will run when redux store changes. So here you don't have to split container and presentation to prevent re render.
Components will also re render if their parent re renders and passes them different props or if parent re renders and the component is a functional component (functional components always re render). But since in the code example the parent (App) never re renders we can define the containers as functional components (no need to wrap in React.memo).
The following is an example of how you can create a connected component similar to react redux connect but instead it'll connect to context:
const {
useMemo,
useState,
useContext,
useRef,
useCallback,
} = React;
const { createSelector } = Reselect;
const MyContext = React.createContext({ count: 0 });
//react-redux connect like function to connect component to context
const connect = (context) => (mapContextToProps) => {
const select = (selector, state, props) => {
if (selector.current !== mapContextToProps) {
return selector.current(state, props);
}
const result = mapContextToProps(state, props);
if (typeof result === 'function') {
selector.current = result;
return select(selector, state, props);
}
return result;
};
return (Component) => (props) => {
const selector = useRef(mapContextToProps);
const contextValue = useContext(context);
const contextProps = select(
selector,
contextValue,
props
);
return useMemo(() => {
const combinedProps = {
...props,
...contextProps,
};
return (
<React.Fragment>
<Component {...combinedProps} />
</React.Fragment>
);
}, [contextProps, props]);
};
};
//connect function that connects to MyContext
const connectMyContext = connect(MyContext);
// simple context that increments a timer
const Context = ({ children }) => {
const [count, setCount] = useState(0);
const increment = useCallback(
() => setCount((c) => c + 1),
[]
);
return (
<MyContext.Provider value={{ count, increment }}>
{children}
</MyContext.Provider>
);
};
//functional component
function FunctionComponent({ count }) {
console.log('in function', count);
return <div>Function Component {count}</div>;
}
//selectors
const selectCount = (state) => state.count;
const selectIncrement = (state) => state.increment;
const selectCountDiveded = createSelector(
selectCount,
(_, divisor) => divisor,
(count, { divisor }) => Math.floor(count / divisor)
);
const createSelectConnectedContext = () =>
createSelector(selectCountDiveded, (count) => ({
count,
}));
//connected component
const ConnectedComponent = connectMyContext(
createSelectConnectedContext
)(FunctionComponent);
//app is also connected but won't re render when count changes
// it only gets increment and that never changes
const App = connectMyContext(
createSelector(selectIncrement, (increment) => ({
increment,
}))
)(function App({ increment }) {
const [divisor, setDivisor] = useState(0.5);
return (
<div>
<button onClick={increment}>increment</button>
<button onClick={() => setDivisor((d) => d * 2)}>
double divisor
</button>
<ConnectedComponent divisor={divisor} />
<ConnectedComponent divisor={divisor * 2} />
<ConnectedComponent divisor={divisor * 4} />
</div>
);
});
ReactDOM.render(
<Context>
<App />
</Context>,
document.getElementById('root')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/reselect/4.0.0/reselect.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>

Related

useEffect rerender the whole table when counter changing

I have some problem with useEffect. When the counter changes it causes the whole table to be rerendered, but i dont pass timer as props in table. How i can prevent this behavior?
function App() {
const dispatch = useDispatch();
const data = useSelector(state => state.data);
const [error, setError] = useState("");
const [counter, setCounter] = useState();
useEffect(() => {
const fetchData = async (setError, setCounter) => {
try {
const response = await axios(url, token);
dispatch(getData(response.data.value));
setError("");
setCounter(180);
} catch(e) {
setError("Error!");
setCounter(180);
}}
fetchData(setError, setCounter);
const interval = setInterval(() => {
fetchData(setError, setCounter);
}, timeToReload * 1000);
const countInterval = setInterval(() =>
setCounter((prev) => prev - 1), 1000)
return () => {
clearInterval(interval);
clearInterval(countInterval);
}
},[dispatch])
const dataForTable = selectorData([...data], {name: sortArrow.columnName, order: sortArrow.sortOrder, type: sortArrow.type})
return (
<div className="App">
<div className="headerWrapper">
<div
className={error ? "LoadingStatus disconnect": "LoadingStatus connect"}>
{error && <div>{error}</div>}
{isFinite(counter) && <div>{"Reload " + counter + " sec"}</div> }
</div>
</div>
<Table specialCategory={specialCategory} data={dataForTable} sortArrow={sortArrow} setSortArrow={setSortArrow}/>
</div>
);
}
export default App;
I trued to useRef without useState, but nothing has changed. Maybe another props in Table component trigger the change?
Imptortant notice: only the body of the table is changed.
When you update the state (e.g. setCounter(...)) of the App component, it causes the entire component with all of it's child-components, including <Table/>, to be re-rendered.
You can either create a new component for everything except the table and put the states which are changing (error and counter) into that file, or memoize the Table component like this:
import { memo } from "react";
function Table(props) {
return (
// [...] Your Table component.
);
}
export default memo(Table);
And import it just as you do already. This will avoid re-rendering the table unless its props change.
See Here the Reasons of React Re-rendering:
If a Parent Component Re-renders, the Children re-render automatically. & because your counter state is on the Table Components parent, It will re-render every time Counter changes.

What is the good practice way to prevent a useEffect from triggering on initial render? [duplicate]

According to the docs:
componentDidUpdate() is invoked immediately after updating occurs. This method is not called for the initial render.
We can use the new useEffect() hook to simulate componentDidUpdate(), but it seems like useEffect() is being ran after every render, even the first time. How do I get it to not run on initial render?
As you can see in the example below, componentDidUpdateFunction is printed during the initial render but componentDidUpdateClass was not printed during the initial render.
function ComponentDidUpdateFunction() {
const [count, setCount] = React.useState(0);
React.useEffect(() => {
console.log("componentDidUpdateFunction");
});
return (
<div>
<p>componentDidUpdateFunction: {count} times</p>
<button
onClick={() => {
setCount(count + 1);
}}
>
Click Me
</button>
</div>
);
}
class ComponentDidUpdateClass extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0,
};
}
componentDidUpdate() {
console.log("componentDidUpdateClass");
}
render() {
return (
<div>
<p>componentDidUpdateClass: {this.state.count} times</p>
<button
onClick={() => {
this.setState({ count: this.state.count + 1 });
}}
>
Click Me
</button>
</div>
);
}
}
ReactDOM.render(
<div>
<ComponentDidUpdateFunction />
<ComponentDidUpdateClass />
</div>,
document.querySelector("#app")
);
<script src="https://unpkg.com/react#16.7.0-alpha.0/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#16.7.0-alpha.0/umd/react-dom.development.js"></script>
<div id="app"></div>
We can use the useRef hook to store any mutable value we like, so we could use that to keep track of if it's the first time the useEffect function is being run.
If we want the effect to run in the same phase that componentDidUpdate does, we can use useLayoutEffect instead.
Example
const { useState, useRef, useLayoutEffect } = React;
function ComponentDidUpdateFunction() {
const [count, setCount] = useState(0);
const firstUpdate = useRef(true);
useLayoutEffect(() => {
if (firstUpdate.current) {
firstUpdate.current = false;
return;
}
console.log("componentDidUpdateFunction");
});
return (
<div>
<p>componentDidUpdateFunction: {count} times</p>
<button
onClick={() => {
setCount(count + 1);
}}
>
Click Me
</button>
</div>
);
}
ReactDOM.render(
<ComponentDidUpdateFunction />,
document.getElementById("app")
);
<script src="https://unpkg.com/react#16.7.0-alpha.0/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#16.7.0-alpha.0/umd/react-dom.development.js"></script>
<div id="app"></div>
You can turn it into custom hooks, like so:
import React, { useEffect, useRef } from 'react';
const useDidMountEffect = (func, deps) => {
const didMount = useRef(false);
useEffect(() => {
if (didMount.current) func();
else didMount.current = true;
}, deps);
}
export default useDidMountEffect;
Usage example:
import React, { useState, useEffect } from 'react';
import useDidMountEffect from '../path/to/useDidMountEffect';
const MyComponent = (props) => {
const [state, setState] = useState({
key: false
});
useEffect(() => {
// you know what is this, don't you?
}, []);
useDidMountEffect(() => {
// react please run me if 'key' changes, but not on initial render
}, [state.key]);
return (
<div>
...
</div>
);
}
// ...
I made a simple useFirstRender hook to handle cases like focussing a form input:
import { useRef, useEffect } from 'react';
export function useFirstRender() {
const firstRender = useRef(true);
useEffect(() => {
firstRender.current = false;
}, []);
return firstRender.current;
}
It starts out as true, then switches to false in the useEffect, which only runs once, and never again.
In your component, use it:
const firstRender = useFirstRender();
const phoneNumberRef = useRef(null);
useEffect(() => {
if (firstRender || errors.phoneNumber) {
phoneNumberRef.current.focus();
}
}, [firstRender, errors.phoneNumber]);
For your case, you would just use if (!firstRender) { ....
Same approach as Tholle's answer, but using useState instead of useRef.
const [skipCount, setSkipCount] = useState(true);
...
useEffect(() => {
if (skipCount) setSkipCount(false);
if (!skipCount) runYourFunction();
}, [dependencies])
EDIT
While this also works, it involves updating state which will cause your component to re-render. If all your component's useEffect calls (and also all of its children's) have a dependency array, this doesn't matter. But keep in mind that any useEffect without a dependency array (useEffect(() => {...}) will be run again.
Using and updating useRef will not cause any re-renders.
#ravi, yours doesn't call the passed-in unmount function. Here's a version that's a little more complete:
/**
* Identical to React.useEffect, except that it never runs on mount. This is
* the equivalent of the componentDidUpdate lifecycle function.
*
* #param {function:function} effect - A useEffect effect.
* #param {array} [dependencies] - useEffect dependency list.
*/
export const useEffectExceptOnMount = (effect, dependencies) => {
const mounted = React.useRef(false);
React.useEffect(() => {
if (mounted.current) {
const unmount = effect();
return () => unmount && unmount();
} else {
mounted.current = true;
}
}, dependencies);
// Reset on unmount for the next mount.
React.useEffect(() => {
return () => mounted.current = false;
}, []);
};
a simple way is to create a let, out of your component and set in to true.
then say if its true set it to false then return (stop) the useEffect function
like that:
import { useEffect} from 'react';
//your let must be out of component to avoid re-evaluation
let isFirst = true
function App() {
useEffect(() => {
if(isFirst){
isFirst = false
return
}
//your code that don't want to execute at first time
},[])
return (
<div>
<p>its simple huh...</p>
</div>
);
}
its Similar to #Carmine Tambasciabs solution but without using state :)
‍‍‍‍‍‍
‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍
function useEffectAfterMount(effect, deps) {
const isMounted = useRef(false);
useEffect(() => {
if (isMounted.current) return effect();
else isMounted.current = true;
}, deps);
// reset on unmount; in React 18, components can mount again
useEffect(() => {
isMounted.current = false;
});
}
We need to return what comes back from effect(), because it might be a cleanup function. But we don't need to determine if it is or not. Just pass it on and let useEffect figure it out.
In an earlier version of this post I said resetting the ref (isMounted.current = false) wasn't necessary. But in React 18 it is, because components can remount with their previous state (thanks #Whatabrain).
I thought creating a custom hook would be overkill and I didn't want to muddle my component's readability by using the useLayoutEffect hook for something unrelated to layouts, so, in my case, I simply checked to see if the value of my stateful variable selectedItem that triggers the useEffect callback is its original value in order to determine if it's the initial render:
export default function MyComponent(props) {
const [selectedItem, setSelectedItem] = useState(null);
useEffect(() => {
if(!selectedItem) return; // If selected item is its initial value (null), don't continue
//... This will not happen on initial render
}, [selectedItem]);
// ...
}
This is the best implementation I've created so far using typescript. Basically, the idea is the same, using the Ref but I'm also considering the callback returned by useEffect to perform cleanup on component unmount.
import {
useRef,
EffectCallback,
DependencyList,
useEffect
} from 'react';
/**
* #param effect
* #param dependencies
*
*/
export default function useNoInitialEffect(
effect: EffectCallback,
dependencies?: DependencyList
) {
//Preserving the true by default as initial render cycle
const initialRender = useRef(true);
useEffect(() => {
let effectReturns: void | (() => void) = () => {};
// Updating the ref to false on the first render, causing
// subsequent render to execute the effect
if (initialRender.current) {
initialRender.current = false;
} else {
effectReturns = effect();
}
// Preserving and allowing the Destructor returned by the effect
// to execute on component unmount and perform cleanup if
// required.
if (effectReturns && typeof effectReturns === 'function') {
return effectReturns;
}
return undefined;
}, dependencies);
}
You can simply use it, as usual as you use the useEffect hook but this time, it won't run on the initial render. Here is how you can use this hook.
useNoInitialEffect(() => {
// perform something, returning callback is supported
}, [a, b]);
If you use ESLint and want to use the react-hooks/exhaustive-deps rule for this custom hook:
{
"rules": {
// ...
"react-hooks/exhaustive-deps": ["warn", {
"additionalHooks": "useNoInitialEffect"
}]
}
}
#MehdiDehghani, your solution work perfectly fine, one addition you have to do is on unmount, reset the didMount.current value to false. When to try to use this custom hook somewhere else, you don't get cache value.
import React, { useEffect, useRef } from 'react';
const useDidMountEffect = (func, deps) => {
const didMount = useRef(false);
useEffect(() => {
let unmount;
if (didMount.current) unmount = func();
else didMount.current = true;
return () => {
didMount.current = false;
unmount && unmount();
}
}, deps);
}
export default useDidMountEffect;
Simplified implementation
import { useRef, useEffect } from 'react';
function MyComp(props) {
const firstRender = useRef(true);
useEffect(() => {
if (firstRender.current) {
firstRender.current = false;
} else {
myProp = 'some val';
};
}, [props.myProp])
return (
<div>
...
</div>
)
}
You can use custom hook to run use effect after mount.
const useEffectAfterMount = (cb, dependencies) => {
const mounted = useRef(true);
useEffect(() => {
if (!mounted.current) {
return cb();
}
mounted.current = false;
}, dependencies); // eslint-disable-line react-hooks/exhaustive-deps
};
Here is the typescript version:
const useEffectAfterMount = (cb: EffectCallback, dependencies: DependencyList | undefined) => {
const mounted = useRef(true);
useEffect(() => {
if (!mounted.current) {
return cb();
}
mounted.current = false;
}, dependencies); // eslint-disable-line react-hooks/exhaustive-deps
};
For people who are having trouble with React 18 strict mode calling the useeffect on the initial render twice, try this:
// The init variable is necessary if your state is an object/array, because the == operator compares the references, not the actual values.
const init = [];
const [state, setState] = useState(init);
const dummyState = useRef(init);
useEffect(() => {
// Compare the old state with the new state
if (dummyState.current == state) {
// This means that the component is mounting
} else {
// This means that the component updated.
dummyState.current = state;
}
}, [state]);
Works in development mode...
function App() {
const init = [];
const [state, setState] = React.useState(init);
const dummyState = React.useRef(init);
React.useEffect(() => {
if (dummyState.current == state) {
console.log('mount');
} else {
console.log('update');
dummyState.current = state;
}
}, [state]);
return (
<button onClick={() => setState([...state, Math.random()])}>Update state </button>
);
}
ReactDOM.createRoot(document.getElementById("app")).render(
<React.StrictMode>
<App />
</React.StrictMode>
);
<script crossorigin src="https://unpkg.com/react#18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#18/umd/react-dom.development.js"></script>
<div id="app"></div>
And in production.
function App() {
const init = [];
const [state, setState] = React.useState(init);
const dummyState = React.useRef(init);
React.useEffect(() => {
if (dummyState.current == state) {
console.log('mount');
} else {
console.log('update');
dummyState.current = state;
}
}, [state]);
return (
<button onClick={() => setState([...state, Math.random()])}>Update state </button>
);
}
ReactDOM.createRoot(document.getElementById("app")).render(
<React.StrictMode>
<App />
</React.StrictMode>
);
<script crossorigin src="https://unpkg.com/react#18/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#18/umd/react-dom.production.min.js"></script>
<div id="app"></div>
If you want to skip the first render, you can create a state "firstRenderDone" and set it to true in the useEffect with empty dependecy list (that works like a didMount). Then, in your other useEffect, you can check if the first render was already done before doing something.
const [firstRenderDone, setFirstRenderDone] = useState(false);
//useEffect with empty dependecy list (that works like a componentDidMount)
useEffect(() => {
setFirstRenderDone(true);
}, []);
// your other useEffect (that works as componetDidUpdate)
useEffect(() => {
if(firstRenderDone){
console.log("componentDidUpdateFunction");
}
}, [firstRenderDone]);
All previous are good, but this can be achieved in a simplier way considering that the action in useEffect can be "skipped" placing an if condition(or any other ) that is basically not run first time, and still with the dependency.
For example I had the case of :
Load data from an API but my title has to be "Loading" till the date were not there, so I have an array, tours that is empty at beginning and show the text "Showing"
Have a component rendered with different information from those API.
The user can delete one by one those info, even all making the tour array empty again as the beginning but this time the API fetch is been already done
Once the tour list is empty by deleting then show another title.
so my "solution" was to create another useState to create a boolean value that change only after the data fetch making another condition in useEffect true in order to run another function that also depend on the tour length.
useEffect(() => {
if (isTitle) {
changeTitle(newTitle)
}else{
isSetTitle(true)
}
}, [tours])
here my App.js
import React, { useState, useEffect } from 'react'
import Loading from './Loading'
import Tours from './Tours'
const url = 'API url'
let newTours
function App() {
const [loading, setLoading ] = useState(true)
const [tours, setTours] = useState([])
const [isTitle, isSetTitle] = useState(false)
const [title, setTitle] = useState("Our Tours")
const newTitle = "Tours are empty"
const removeTours = (id) => {
newTours = tours.filter(tour => ( tour.id !== id))
return setTours(newTours)
}
const changeTitle = (title) =>{
if(tours.length === 0 && loading === false){
setTitle(title)
}
}
const fetchTours = async () => {
setLoading(true)
try {
const response = await fetch(url)
const tours = await response.json()
setLoading(false)
setTours(tours)
}catch(error) {
setLoading(false)
console.log(error)
}
}
useEffect(()=>{
fetchTours()
},[])
useEffect(() => {
if (isTitle) {
changeTitle(newTitle)
}else{
isSetTitle(true)
}
}, [tours])
if(loading){
return (
<main>
<Loading />
</main>
)
}else{
return (
<main>
<Tours tours={tours} title={title} changeTitle={changeTitle}
removeTours={removeTours} />
</main>
)
}
}
export default App
const [dojob, setDojob] = useState(false);
yourfunction(){
setDojob(true);
}
useEffect(()=>{
if(dojob){
yourfunction();
setDojob(false);
}
},[dojob]);

I would expect this useRef and useEffect combo to fail...why does it succeed?

I would expect this useEffect to fail on the first render, since I would assume the innerCarouselRef.current would be undefined on the first render and it makes a call to getBoundingClientRect. Why does it work/why is the innerCarouselRef.current defined when the useEffect runs?
import React from 'react';
import { debounce } from 'lodash';
export default function Carousel({ RenderComponent }) {
const [innerCarouselWidth, setInnerCarouselWidth] = React.useState(0);
const [itemWidth, setItemWidth] = React.useState(0);
const innerCarouselRef = useRef();
const itemRef = useRef();
const content = data.map((el, i) => {
return (
<div key={`item-${i}`} ref={i === 0 ? itemRef : undefined}>
<RenderComponent {...el} />
</div>
);
});
useEffect(() => {
const getElementWidths = () => {
setInnerCarouselWidth(innerCarouselRef.current.getBoundingClientRect().width); // why doesn't this call to getBoundingClientRect() break?
setItemWidth(itemRef.current.getBoundingClientRect().width);
};
getElementWidths();
const debouncedListener = debounce(getElementWidths, 500);
window.addEventListener('resize', debouncedListener);
return () => window.removeEventListener('resize', debouncedListener);
}, []);
return (
<div className="inner-carousel" ref={innerCarouselRef}>
{content}
</div>
)
}
React runs the effects after it has updated the DOM (we typically want it to work that way). In your case, the effect runs after the component has mounted and so innerCarouselRef.current is set.
I would recommend reading the useEffect docs to gain a better understanding.

React Sequential Rendering Hook

I've got some components which need to render sequentially once they've loaded or marked themselves as ready for whatever reason.
In a typical {things.map(thing => <Thing {...thing} />} example, they all render at the same time, but I want to render them one by one I created a hook to to provide a list which only contains the sequentially ready items to render.
The problem I'm having is that the children need a function in order to tell the hook when to add the next one into its ready to render state. This function ends up getting changed each time and as such causes an infinite number of re-renders on the child components.
In the examples below, the child component useEffect must rely on the dependency done to pass the linter rules- if i remove this it works as expected because done isn't a concern whenever it changes but obviously that doesn't solve the issue.
Similarly I could add if (!attachment.__loaded) { into the child component but then the API is poor for the hook if the children need specific implementation such as this.
I think what I need is a way to stop the function being recreated each time but I've not worked out how to do this.
Codesandbox link
useSequentialRenderer.js
import { useReducer, useEffect } from "react";
const loadedProperty = "__loaded";
const reducer = (state, {i, type}) => {
switch (type) {
case "ready":
const copy = [...state];
copy[i][loadedProperty] = true;
return copy;
default:
return state;
}
};
const defaults = {};
export const useSequentialRenderer = (input, options = defaults) => {
const [state, dispatch] = useReducer(options.reducer || reducer, input);
const index = state.findIndex(a => !a[loadedProperty]);
const sliced = index < 0 ? state.slice() : state.slice(0, index + 1);
const items = sliced.map((item, i) => {
function done() {
dispatch({ type: "ready", i });
return i;
}
return { ...item, done };
});
return { items };
};
example.js
import React, { useEffect, useState } from "react";
import ReactDOM from "react-dom";
import { useSequentialRenderer } from "./useSequentialRenderer";
const Attachment = ({ children, done }) => {
const [loaded, setLoaded] = useState(false);
useEffect(() => {
const delay = Math.random() * 3000;
const timer = setTimeout(() => {
setLoaded(true);
const i = done();
console.log("happening multiple times", i, new Date());
}, delay);
return () => clearTimeout(timer);
}, [done]);
return <div>{loaded ? children : "loading"}</div>;
};
const Attachments = props => {
const { items } = useSequentialRenderer(props.children);
return (
<>
{items.map((attachment, i) => {
return (
<Attachment key={attachment.text} done={() => attachment.done()}>
{attachment.text}
</Attachment>
);
})}
</>
);
};
function App() {
const attachments = [1, 2, 3, 4, 5, 6, 7, 8].map(a => ({
loaded: false,
text: a
}));
return (
<div className="App">
<Attachments>{attachments}</Attachments>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
Wrap your callback in an aditional layer of dependency check with useCallback. This will ensure a stable identity across renders
const Component = ({ callback }) =>{
const stableCb = useCallback(callback, [])
useEffect(() =>{
stableCb()
},[stableCb])
}
Notice that if the signature needs to change you should declare the dependencies as well
const Component = ({ cb, deps }) =>{
const stableCb = useCallback(cb, [deps])
/*...*/
}
Updated Example:
https://codesandbox.io/s/wizardly-dust-fvxsl
Check if(!loaded){.... setTimeout
or
useEffect with [loaded]);
useEffect(() => {
const delay = Math.random() * 1000;
const timer = setTimeout(() => {
setLoaded(true);
const i = done();
console.log("rendering multiple times", i, new Date());
}, delay);
return () => clearTimeout(timer);
}, [loaded]);
return <div>{loaded ? children : "loading"}</div>;
};

React custom Hook using useRef returns null for the first time the calling component Loads?

I have created a custom hook to scroll the element back into view when the component is scrolled.
export const useComponentIntoView = () => {
const ref = useRef();
const {current} = ref;
if (current) {
window.scrollTo(0, current.offsetTop );
}
return ref;
}
Now i am making use of this in a functional component like
<div ref={useComponentIntoView()}>
So for the first time the current always comes null, i understand that the component is still not mounted so the value is null . but what can we do to get this values always in my custom hook as only for the first navigation the component scroll doesn't work . Is there any work around to this problem .
We need to read the ref from useEffect, when it has already been assigned. To call it only on mount, we pass an empty array of dependencies:
const MyComponent = props => {
const ref = useRef(null);
useEffect(() => {
if (ref.current) {
window.scrollTo(0, ref.current.offsetTop);
}
}, []);
return <div ref={ref} />;
};
In order to have this functionality out of the component, in its own Hook, we can do it this way:
const useComponentIntoView = () => {
const ref = useRef(null);
useEffect(() => {
if (ref.current) {
window.scrollTo(0, ref.current.offsetTop);
}
}, []);
return ref;
};
const MyComponent = props => {
const ref = useComponentIntoView();
return <div ref={ref} />;
};
We could also run the useEffect hook after a certain change. In this case we would need to pass to its array of dependencies, a variable that belongs to a state. This variable can belong to the same Component or an ancestor one. For example:
const MyComponent = props => {
const [counter, setCounter] = useState(0);
const ref = useRef(null);
useEffect(() => {
if (ref.current) {
window.scrollTo(0, ref.current.offsetTop);
}
}, [counter]);
return (
<div ref={ref}>
<button onClick={() => setCounter(counter => counter + 1)}>
Click me
</button>
</div>
);
};
In the above example each time the button is clicked it updates the counter state. This update triggers a new render and, as the counter value changed since the last time useEffect was called, it runs the useEffect callback.
As you mention, ref.current is null until after the component is mounted. This is where you can use useEffect - which will fire after the component is mounted, i.e.:
const useComponentIntoView = () => {
const ref = useRef();
useEffect(() => {
if (ref.current) {
window.scrollTo(0, ref.current.offsetTop );
}
});
return ref;
}

Resources