Consider the following component:
function SomeComponent(props){
const [isMouseOver, setIsMouseOver] = useState(false);
return (
<div onMouseOver={_ => setIsMouseOver(true)}
onMouseOut={_ => setIsMouseOver(false)}>
<img src={isMouseOver ? EditIconHover : EditIcon} alt="icon"/>
</div>
);
}
New instance of arrow function is created on every render. It creates a closure over setIsMouseOver function, though this function never changes.
Sure, it does not drastically affect performance in this case, but I'd like to know how to avoid these unnecessary memory allocations.
Do I have to attach all dependencies required for event handler to DOM element
<div data-deps={setIsMouseOver} onMouseOver={onMouseOverHandler} onMouseOut={onMouseOutHandler}></div>
and then access deps property inside onMouseOverHandler and onMouseOutHandler functions?
You might want to use memoization, although in this particular example its an overhead.
Refer to useCallback
import React, { useState, useCallback, useRef, useEffect } from 'react';
import ReactDOM from 'react-dom';
const App = () => {
const [isEnabled, setIsEnabled] = useState(false);
const toggle = useCallback(() => setIsEnabled(v => !v), []);
const toggleRef = useRef();
const setterRef = useRef();
useEffect(() => {
toggleRef.current = toggle;
setterRef.current = setIsEnabled;
}, [toggle]);
useEffect(() => {
console.log(toggle === toggleRef.current);
console.log(setIsEnabled === setterRef.current);
});
return <button onClick={toggle}>{isEnabled ? 'Enabled' : 'Disabled'}</button>;
};
ReactDOM.render(<App />, document.getElementById('root'));
Related
I have a basic React code that create an empty list and fill it with data from the db with an useEffect. My problem is that when I try to make a map with the elements and render a new component, if I use an useState I can't do it.
Here is my code:
import "./../../../../assets/styles/logged/pedidos/pedidos.min.css"
import { Context } from "../../../../App";
import { useContext, useEffect, useState } from "react";
import { PedidoCamionInterface } from "../../../../domain/entities/pedido_camion/pedido_camion_interface";
export function Pedidos() {
const providers = useContext(Context);
const [pedidos, setPedidos] = useState<PedidoCamionInterface[]>([]);
useEffect(() => {
//TODO
providers.providers.pedidosDb.getPedidosCamion([1, 2, 3], "").then((pedidos) => {
setPedidos(pedidos);
});
}, []);
return (
<div id="content">
<h1>Pedidos de camiĆ³n</h1>
<div id="pedidos">
{pedidos.map((pedido: PedidoCamionInterface) => { return pedidoDiv(pedido); })}
</div>
</div>
);
}
const pedidoDiv = (pedido: PedidoCamionInterface) => {
const [selected, setSelected] = useState<boolean>(false);
return (
<div className={`pedido ${selected ? "selected" : ""}`} key={pedido.idPedido}>
<span>{pedido.nombrePedido}</span>
<span>{pedido.tienda}</span>
</div>
);
}
This is the error that I get:
Warning: React has detected a change in the order of Hooks called by Pedidos. This will lead to bugs and errors if not fixed. For more information, read the Rules of Hooks: https://reactjs.org/link/rules-of-hooks
Previous render Next render
------------------------------------------------------
1. useContext useContext
2. useState useState
3. useEffect useEffect
4. undefined useState
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Pedidos#http://localhost:5173/src/ui/pages/logged/pedidos/pedidos.tsx?t=1667424544020:22:31
RenderedRoute#http://localhost:5173/node_modules/.vite/deps/react-router-dom.js?v=396bef63:2437:7
Routes#http://localhost:5173/node_modules/.vite/deps/react-router-dom.js?v=396bef63:2746:7
Router#http://localhost:5173/node_modules/.vite/deps/react-router-dom.js?v=396bef63:2697:7
BrowserRouter#http://localhost:5173/node_modules/.vite/deps/react-router-dom.js?v=396bef63:3079:7
App#http://localhost:5173/src/App.tsx?t=1667424544020:30:35 react-dom.development.js:86:29
Thanks for all.
Be able to render the child
I think the problem might lie in the .map() function.
Instead of
{pedidos.map((pedido: PedidoCamionInterface) => { return pedidoDiv(pedido); })}
try
{pedidos.map((pedido: PedidoCamionInterface) => (<PedidoDiv pedido={pedido} />))}
so React knows to render the component.
Also, change
const pedidoDiv = (pedido: PedidoCamionInterface) => {
to
const pedidoDiv = ({ pedido }: PedidoCamionInterface) => {
so the property pedido is destructured.
Within ParentComponent, I render a chart (ResponsiveLine). I have a function (calculateHeight) calculating the height of some DOM elements of the chart.
To work fine, my function calculateHeight have to be triggered once the chart ResponsiveLine is rendered.
Here's my issue: useEffect will trigger before the child is done rendering, so I can't calculate the size of the DOM elements of the chart.
How to trigger my function calculateHeight once the chart ResponsiveLine is done rendering?
Here's a simplified code
const ParentComponent = () => {
const myref = useRef(null);
const [marginBottom, setMarginBottom] = useState(60);
useEffect(() => {
setMarginBottom(calculateHeight(myref));
});
return (
<div ref={myref}>
<ResponsiveLine marginBottom={marginBottom}/>
</div>)
}
EDIT
I can't edit the child ResponsiveLine, it's from a library
You can use the ResizeObserver API to track changes to the dimensions of the box of the div via its ref (specifically the height, which is the block size dimension for content which is in a language with a horizontal writing system like English). I won't go into the details of how the API works: you can read about it at the MDN link above.
The ResponsiveLine aspect of your question doesn't seem relevant except that it's a component you don't control and might change its state asynchronously. In the code snippet demonstration below, I've created a Child component that changes its height after 2 seconds to simulate the same idea.
Code in the TypeScript playground
<div id="root"></div><script src="https://unpkg.com/react#18.2.0/umd/react.development.js"></script><script src="https://unpkg.com/react-dom#18.2.0/umd/react-dom.development.js"></script><script src="https://unpkg.com/#babel/standalone#7.18.5/babel.min.js"></script><script>Babel.registerPreset('tsx', {presets: [[Babel.availablePresets['typescript'], {allExtensions: true, isTSX: true}]]});</script>
<script type="text/babel" data-type="module" data-presets="tsx,react">
// import ReactDOM from 'react-dom/client';
// import {useEffect, useRef, useState, type ReactElement} from 'react';
// This Stack Overflow snippet demo uses UMD modules instead of the above import statments
const {useEffect, useRef, useState} = React;
// You didn't show this function, so I don't know what it does.
// Here's something in place of it:
function calculateHeight (element: Element): number {
return element.getBoundingClientRect().height;
}
function Child (): ReactElement {
const [style, setStyle] = useState<React.CSSProperties>({
border: '1px solid blue',
height: 50,
});
useEffect(() => {
// Change the height of the child element after 2 seconds
setTimeout(() => setStyle(style => ({...style, height: 150})), 2e3);
}, []);
return (<div {...{style}}>Child</div>);
}
function Parent (): ReactElement {
const ref = useRef<HTMLDivElement>(null);
const [marginBottom, setMarginBottom] = useState(60);
useEffect(() => {
if (!ref.current) return;
let lastBlockSize = 0;
const observer = new ResizeObserver(entries => {
for (const entry of entries) {
if (!(entry.borderBoxSize && entry.borderBoxSize.length > 0)) continue;
// #ts-expect-error
const [{blockSize}] = entry.borderBoxSize;
if (blockSize === lastBlockSize) continue;
setMarginBottom(calculateHeight(entry.target));
lastBlockSize = blockSize;
}
});
observer.observe(ref.current, {box: 'border-box'});
return () => observer.disconnect();
}, []);
return (
<div {...{ref}}>
<div>height: {marginBottom}px</div>
<Child />
</div>
);
}
const reactRoot = ReactDOM.createRoot(document.getElementById('root')!);
reactRoot.render(<Parent />);
</script>
You said,
Here's my issue: useEffect will trigger before the child is done rendering, so I can't calculate the size of the DOM elements of the chart.
However, parent useEffect does not do that, It fires only after all the children are mounted and their useEffects are fired.
The value of myref is stored in myref.current So your useEffect should be
useEffect(() => {
setMarginBottom(calculateHeight(myref.current));
});
Why don't you send a function to the child component that is called from the useEffect of the child component.
const ParentComponent = () => {
const myref = useRef(null);
const [marginBottom, setMarginBottom] = useState(60);
someFunction = () => {
setMarginBottom(calculateHeight(myref));
}
return (
<div ref={myref}>
<ResponsiveLine func={someFunction} marginBottom={marginBottom}/>
</div>)
}
// CHILD COMPONENT
const ChildComponent = ({func, marginBotton}) => {
const [marginBottom, setMarginBottom] = useState(60);
useEffect(() => {
func();
}, []);
return <div></div>
}
Whenever I update user (object) in users (array) in context - all components which uses users re-renders.
What I've tried
I have a component which is using values from context:
const DashboardCardList=()=> {
const context = useContext(StaticsContext);
const users = context.users.filter(user=>user.visible);
return !users
? <Loading/>
: (
<Container>
{users.map(user=>
<DashboardCard key={user._id} user={user}/>
)}
</Container>
);
};
My update function (updates context state):
const onUserUpdate=(user)=>{
const index = this.state.users.findIndex(item => item._id === user._id);
const users = [...this.state.users]
users[index] = user;
this.setState({users:users});
}
Final component:
const DashboardCard=({user})=> {
console.log("I'm here!", user);
return ...;
}
Question
Why it keeps re-rendering? Is it because of context?
How to write this properly?
There is no render bailout for context consumers (v17).
Here is a demonstration, where the Consumer will always re-render just because he is a Context Consumer, even though he doesn't consume anything.
import React, { useState, useContext, useMemo } from "react";
import ReactDOM from "react-dom";
// People wonder why the component gets render although the used value didn't change
const Context = React.createContext();
const Provider = ({ children }) => {
const [counter, setCounter] = useState(0);
const value = useMemo(() => {
const count = () => setCounter(p => p + 1);
return [counter, count];
}, [counter]);
return <Context.Provider value={value}>{children}</Context.Provider>;
};
const Consumer = React.memo(() => {
useContext(Context);
console.log("rendered");
return <>Consumer</>;
});
const ContextChanger = () => {
const [, count] = useContext(Context);
return <button onClick={count}>Count</button>;
};
const App = () => {
return (
<Provider>
<Consumer />
<ContextChanger />
</Provider>
);
};
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById("root")
);
To fix it:
Use a single context for each consumed value. Meaning that context holds a single value, (and no, there is no problem with multiple contexts in an application).
Use a state management solution like Recoil.js, Redux, MobX, etc. (although it might be overkill, think good about app design beforehand).
Minor optimization can be achieved by memoizing Provider's values with useMemo.
Whenever I update user (object) in users (array) in context - all components which uses users re-renders.
What I've tried
I have a component which is using values from context:
const DashboardCardList=()=> {
const context = useContext(StaticsContext);
const users = context.users.filter(user=>user.visible);
return !users
? <Loading/>
: (
<Container>
{users.map(user=>
<DashboardCard key={user._id} user={user}/>
)}
</Container>
);
};
My update function (updates context state):
const onUserUpdate=(user)=>{
const index = this.state.users.findIndex(item => item._id === user._id);
const users = [...this.state.users]
users[index] = user;
this.setState({users:users});
}
Final component:
const DashboardCard=({user})=> {
console.log("I'm here!", user);
return ...;
}
Question
Why it keeps re-rendering? Is it because of context?
How to write this properly?
There is no render bailout for context consumers (v17).
Here is a demonstration, where the Consumer will always re-render just because he is a Context Consumer, even though he doesn't consume anything.
import React, { useState, useContext, useMemo } from "react";
import ReactDOM from "react-dom";
// People wonder why the component gets render although the used value didn't change
const Context = React.createContext();
const Provider = ({ children }) => {
const [counter, setCounter] = useState(0);
const value = useMemo(() => {
const count = () => setCounter(p => p + 1);
return [counter, count];
}, [counter]);
return <Context.Provider value={value}>{children}</Context.Provider>;
};
const Consumer = React.memo(() => {
useContext(Context);
console.log("rendered");
return <>Consumer</>;
});
const ContextChanger = () => {
const [, count] = useContext(Context);
return <button onClick={count}>Count</button>;
};
const App = () => {
return (
<Provider>
<Consumer />
<ContextChanger />
</Provider>
);
};
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById("root")
);
To fix it:
Use a single context for each consumed value. Meaning that context holds a single value, (and no, there is no problem with multiple contexts in an application).
Use a state management solution like Recoil.js, Redux, MobX, etc. (although it might be overkill, think good about app design beforehand).
Minor optimization can be achieved by memoizing Provider's values with useMemo.
I have a component MyContainer which has a state variable (defined via useState hook), defines a context provider to which it passes the state variable as value and contains also 2 children, MySetCtxComponent and MyViewCtxComponent.
MySetCtxComponent can change the value stored in the context invoking a set function which is also passed as part of the context, BUT DOES NOT RENDER it.
MyViewCtxComponent, on the contrary, RENDERS the value stored in the context.
MySetCtxComponent defines also an effect via useEffect hook. This effect is, for instance, used to update the value of the context at a fixed interval of time.
So the code of the 3 components is this
MyContainer
export function MyContainer() {
const [myContextValue, setMyContextValue] = useState<string>(null);
const setCtxVal = (newVal: string) => {
setMyContextValue(newVal);
};
return (
<MyContext.Provider
value={{ value: myContextValue, setMyContextValue: setCtxVal }}
>
<MySetCtxComponent />
<MyViewCtxComponent />
</MyContext.Provider>
);
}
MySetCtxComponent
(plus a global varibale to make the example simpler)
let counter = 0;
export function MySetCtxComponent() {
const myCtx = useContext(MyContext);
useEffect(() => {
console.log("=======>>>>>>>>>>>> Use Effect run in MySetCtxComponent");
const intervalID = setInterval(() => {
myCtx.setMyContextValue("New Value " + counter);
counter++;
}, 3000);
return () => clearInterval(intervalID);
}, [myCtx]);
return <button onClick={() => (counter = 0)}>Reset</button>;
}
MyViewCtxComponent
export function MyViewCtxComponent() {
const myCtx = useContext(MyContext);
return (
<div>
This is the value of the contex: {myCtx.value}
</div>
);
}
Now my problem is that, in this way, everytime the context is updated the effect of MySetCtxComponent is run again even if this is not at all required since MySetCtxComponent does not need to render when the context is updated. But, if I remove myCtx from the dependency array of the useEffect hook (which prevents the effect hook when the context get updated), then I get an es-lint warning such as React Hook useEffect has a missing dependency: 'myCtx'. Either include it or remove the dependency array react-hooks/exhaustive-deps.
Finally the question: is this a case where it is safe to ignore the warning or do I have a fundamental design error here and maybe should opt to use a store? Consider that the example may look pretty silly, but it is the most stripped down version of a real scenario.
Here a stackblitz to replicate the case
One pattern for solving this is to split the context in two, providing one context for actions and another for accessing the context value. This allows you to fulfill the expected dependency array of the useEffect correctly, while also not running it unnecessarily when only the context value has changed.
const { useState, createContext, useContext, useEffect, useRef } = React;
const ViewContext = createContext();
const ActionsContext = createContext();
function MyContainer() {
const [contextState, setContextState] = useState();
return (
<ViewContext.Provider value={contextState}>
<ActionsContext.Provider value={setContextState}>
<MySetCtxComponent />
<MyViewCtxComponent />
</ActionsContext.Provider>
</ViewContext.Provider>
)
}
function MySetCtxComponent() {
const setContextState = useContext(ActionsContext);
const counter = useRef(0);
useEffect(() => {
console.log("=======>>>>>>>>>>>> Use Effect run in MySetCtxComponent");
const intervalID = setInterval(() => {
setContextState("New Value " + counter.current);
counter.current++;
}, 1000);
return () => clearInterval(intervalID);
}, [setContextState]);
return <button onClick={() => (counter.current = 0)}>Reset</button>;
}
function MyViewCtxComponent() {
const contextState = useContext(ViewContext);
return (
<div>
This is the value of the context: {contextState}
</div>
);
}
ReactDOM.render(
<MyContainer />,
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>
The problem is what you're passing to the useEffect dependency array in MySetCtxComponent. You should only pass the update function as shown below.
However, personally I would destructure out the setter as it's more readable and naturally avoids this issue.
const { useState, createContext, useContext, useEffect, useRef, useCallback } = React;
const MyContext = createContext();
function MyContainer() {
const [myContextValue, setMyContextValue] = useState(null);
// this function is currently unnecessary, but left in because I assume you change the functions default behvaiour in your real code
// also this should be wrapped in a useCallback if used
const setCtxVal = useCallback((newVal: string) => {
setMyContextValue(newVal);
}, [setMyContextValue]);
return (
<MyContext.Provider value={{ value: myContextValue, setMyContextValue: setCtxVal }}>
<MySetCtxComponent />
<MyViewCtxComponent />
</MyContext.Provider>
)
}
function MySetCtxComponent() {
const myCtx = useContext(MyContext);
// or const { setMyContextValue } = useContext(MyContext);
const counter = useRef(0);
useEffect(() => {
console.log("=======>>>>>>>>>>>> Use Effect run in MySetCtxComponent");
const intervalID = setInterval(() => {
myCtx.setMyContextValue("New Value " + counter.current);
// or setMyContextValue("New Value " + counter.current);
counter.current++;
}, 1000);
return () => clearInterval(intervalID);
}, [myCtx.setMyContextValue, /* or setMyContextValue */]);
return <button onClick={() => (counter.current = 0)}>Reset</button>;
}
function MyViewCtxComponent() {
const myCtx = useContext(MyContext);
return (
<div>
This is the value of the context: {myCtx.value}
</div>
);
}
ReactDOM.render(
<MyContainer />,
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>