import React, { useState } from 'react';
import Tab from 'react-bootstrap/Tab';
import Tabs from 'react-bootstrap/Tabs';
import { sections } from '../../data/sections';
export const NavigationTop = () => {
const [mySections, setMySections] = useState(sections);
const selectSection = (id) => {
let newSections = mySections;
newSections[id].name = mySections[id].name + '*';
setMySections(newSections);
};
return (
<Tabs defaultActiveKey="0" id="fill-tab-example" className="mb-3" onSelect={(k) => selectSection(k)} fill>
{mySections.map((el) => {
const { id, name } = el;
return (
<Tab id={id} key={id} eventKey={id} title={name}></Tab>
);
})}
</Tabs>
);
}
The selectSection event is triggered and newSections contains the new values, but the page does not show the new values.
Where is my error?
You are mutating the state object and not providing a new array reference for React's reconciliation process to trigger a component rerender.
const [mySections, setMySections] = useState(sections);
const selectSection = (id) => {
let newSections = mySections; // <-- reference to state
newSections[id].name = mySections[id].name + '*'; // <-- mutations
setMySections(newSections); // <-- same reference
};
The mySections state reference never changes so React bails on rerendering the component. Shallow copy all state, and nested state, that is being updated.
Use a functional state update to correctly update from any previous state.
Example:
const selectSection = (id) => {
setMySections(sections => sections.map(section =>
section.id === id
? { ...section, name: section.name + "*" }
: section
));
};
try this change
let newSections = [...mySections];
what this does is make a copy of your array.
if you don't make a copy, reference doesn't change, and react does a shallow comparison ( only checks the reference and not value ) to see if UI needs to be updated.
Related
I have an array of objects called data. I loop this array and render the Counter component. Increment and decrement of the counter value are passed as props to the component.
But if I change the value in a one-component, the other two components also re-renders. Which is not needed. How do I prevent this behavior? I tried memo and useCallback but seems not implemented correctly.
Counter.js
import React, { useEffect } from "react";
const Counter = ({ value, onDecrement, onIncrement, id }) => {
useEffect(() => {
console.log("Function updated!", id);
}, [onDecrement, onIncrement]);
return (
<div>
{value}
<button onClick={() => onDecrement(id)}>-</button>
<button onClick={() => onIncrement(id)}>+</button>
</div>
);
};
export default React.memo(Counter);
Home.js
import React, { useState, useCallback } from "react";
import Counter from "../components/Counter";
export default function Home() {
const [data, setData] = useState([
{
id: 1,
value: 0,
},
{
id: 2,
value: 0,
},
{
id: 3,
value: 0,
},
]);
const onIncrement = useCallback(
(id) => {
setData((e) =>
e.map((record) => {
if (record.id === id) {
record.value += 1;
}
return record;
})
);
},
[data]
);
const onDecrement = useCallback(
(id) => {
setData((e) =>
e.map((record) => {
if (record.id === id) {
record.value -= 1;
}
return record;
})
);
},
[data]
);
return (
<div>
<h1>Home</h1>
{data.map((e) => {
return (
<Counter
value={e.value}
onDecrement={onDecrement}
onIncrement={onIncrement}
id={e.id}
/>
);
})}
</div>
);
}
I suspect useCallback & useMemo are not helpful in this case, since you're running an inline function in your render:
{data.map(e => <Counter ...>)}
This function will always returns a fresh array & the component will always be different than the previous one.
In order to fix this, I think you'd want to memoize that render function, not the Counter component.
Here's a simple memoized render function with useRef:
// inside of a React component
const cacheRef = useRef({})
const renderCounters = (data) => {
let results = []
data.forEach(e => {
const key = `${e.id}-${e.value}`
const component = cacheRef.current[key] || <Counter
value={e.value}
key={e.id}
onDecrement={onDecrement}
onIncrement={onIncrement}
id={e.id}
/>
results.push(component)
cacheRef.current[key] = component
})
return results
}
return (
<div>
<h1>Home</h1>
{renderCounters(data)}
</div>
);
In the codesandbox below, only the clicked component log its id:
https://codesandbox.io/s/vibrant-wildflower-0djo4?file=/src/App.js
Disclaimer: With this implementation, the component will only rerender if its data value changes. Other props (such as the increment/decrement callbacks) will not trigger changes. There's also no way to clear the cache.
Memoize is also trading memory for performance — sometimes it's not worth it. If there could be thousands of Counter, there're better optimiztion i.e. changing UI design, virtualizing the list, etc.
I'm sure there's a way to do this with useMemo/React.memo but I'm not familiar with it
I have a component which takes in a matrix as an input (actually an object which has matrix and getter and setter, but that is irrelevant) and renders a table based on it. The table cells should be text inputs and should change the matrix when the value in them is changed. How do I do that without using a force update? Here is sample working code (this is the App.js in a create-react-app):
import React, {useState} from "react";
const nodes = [
[1,2,3],
[4,5,6],
[7,8,9]
];
function useForceUpdate() {
const [flag, setFlag] = useState(false);
return () => setFlag(!flag);
}
function Component({nodes}) {
const forceUpdate = useForceUpdate();
const handleOnClick = (i,j) => {
nodes[i][j]++;
forceUpdate();
};
return (
<thead>
{nodes.map((row,i)=>
<tr>
{row.map((el,j)=>
<td onClick={()=>handleOnClick(i, j)}>{el}</td>
)}
</tr>
)}
</thead>
);
}
function App() {
return (
<Component nodes={nodes} />
);
}
export default App;
You can try it on CodePen from this link: https://codepen.io/askenderski/pen/dyNRyxJ
I think the right way to go about it would be to have nodes as a state and pass it and a setNodes as a prop to your Component and then trigger the update from your child like so (take a note of the newNode as a copy of nodes) :-
function Component({nodes,setNodes}) {
const handleOnClick = (i,j) => {
let newNodes = nodes.map(row=>[...row]);
newNodes[i][j]+=1;
setNodes(newNodes);
};
return (
<thead>
{nodes.map((row,i)=>
<tr>
{row.map((el,j)=>
<td onClick={()=>handleOnClick(i, j)}>{el}</td>
)}
</tr>
)}
</thead>
);
}
function App() {
const [nodes,setNodes] = React.useState([
[1,2,3],
[4,5,6],
[7,8,9]
]);
return ( <Component nodes={nodes} setNodes={setNodes} />
);
}
ReactDOM.render(
<App></App>,
document.getElementById('root')
);
So if your source of truth comes from above (props instead of state) then you need to pass down a handler as well:
const SomeParentComponent = () => {
// making an assumption that the immediate parent
// holds the nodes as state - it could come from anywhere
// "above" your component
const [matrix,setMatrix] = useState(theMatrix);
// this is the handler that will update your state
const handleChangeNode = (i,j) => {
// always reurn a new matrix, which is why you do matrix.map
setMatrix(matrix => matrix.map((row,idx) => {
// not interested in changing this one, return the same reference
if(idx !== i) return e;
// change this one
return row.map((col,idx2) => {
// not interested in changing this one
if(idx2 !== j) return col;
// change this one
return col++;
});
});
}
return <YourComponent nodes={matrix} handleChangeNode={handleChangeNode}/>
}
Don't mutate stuff in react. If you want to update an object/array (or some property nested within that object/array) always return a new object/array.
Just useState is enough for that
Demo link
const [nodeList,setNodeList] = React.useState(nodes);
const handleOnClick = (i,j) => {
let nodesCopy = [...nodeList];
nodesCopy[i][j]++;
setNodeList(nodesCopy)
};
import React, { useState } from "react";
import useInterval from "use-interval";
const useOwnHook = () => {
const arr = [...Array(100)].map((_, index) => index);
return {
arr
};
};
const Component = ({ count }) => {
const { arr } = useOwnHook();
console.log(arr, "arr");
return (
<div className="App">
<h1>{count + 1}</h1>
</div>
);
};
export default function App() {
const [count, setCount] = useState(0);
useInterval(() => {
setCount(count + 1);
}, 1000);
return <Component count={count} />;
}
I've created hook useOwnHook for demonstration that each time Component re-render, each time it goes insite useOwnHook, and create new array, is it possible to prevent it to move inside of this hook each time on re-render?
Personally, I'd use a state variable with a init function:
const useOwnHook = () => {
const [arr] = useState(() => [...Array(100)].map((_, index) => index));
return {
arr
};
};
Benefit of the init function is that it's lazy-evaluated so you won't be constructing the array each time the component is rendered.
You can add useState into your custom hook as:
const useOwnHook = () => {
const [arr] = useState([...Array(100)].map((_, index) => index));
return {
arr
};
};
By doing this you can keep the same array in your useOwnHook.
Also you can import as import { useState } from 'react'.
See also from the Using the State Hook documentation - example with a different variable:
We declare a state variable called count, and set it to 0. React will remember its current value between re-renders, and provide the most recent one to our function. If we want to update the current count, we can call setCount.
I am setupping a simple dashboard to challeging my self with ReactJS, but I have some issues preventing useless re-rendering.
I have a root component called App where I fetch some data.
const App = () => {
const [data, setData] = useState(null);
const [list1, setList1] = useState(null);
const [list2, setList2] = useState(null);
const [list3, setList3] = useState(null);
const [list4, setList4] = useState(null);
useEffect(() => {
const fetchData = fetchDataInSomeWay();
const fetchedData = getData(fetchData);
const list1Data = getList1(fetchData);
setList1(list1Data);
setData(fetchedData);
});
...
{ data !== null
&& (
<Parent
data={data}
list1={list1}
list2={list2}
list3={list3}
list4={list4}
/>
};
Then I setup a Parent component where I created some Select component and other elements which depend on the values selected by select.
I have a Select element for each list state created with useState();
const Google = ({
data,
list1,
list2,
list3,
list4,
}) => {
const [typeValue, setTypeValue] = useState('someValue');
const [list1Value, setList1Value] = useState(list1[0]);
const [list2Value, setList2Value] = useState(list2[0]);
const [list3Value, setList3Value] = useState(list3[0]);
const [list4Value, setList4Value] = useState(list4[0]);
const onChangeSelectTypeValue = (value) => {
setTypeValue(value);
};
...
const selectTypeValueElement = (
<SelectElement
select={selectType}
value={[typeValue]}
onChangeValue={onChangeSelectTypeValue}
values={list1Value}
/>
);
...
<div className="interactionHeaderChart">
{ selectTypeValueElement }
...
</div>
};
Then I have a Select element where I do not store a state, but where option selected is passed to Parent compoment.
const SelectElement = ({
select, value, values, onChangeValue,
}) => {
...
<Select
...
value={value[0]}
onChange={onChangeValue}
>
...
};
Now when I select some option from one Select, state of Parent change and all Childs re-render, all Selects components and also other components which depend on the values selected by select.
Can I prevent all Select components from re-rendering? Can I avoid to re-render all other components which does not depend on the values of option selected?
The fact that the state has changed from the onChange function and not from useEffect() is confusing me and I can not understand how to solve it.
Thanks.
You should look into shouldComponentUpdate:
https://reactjs.org/docs/react-component.html#shouldcomponentupdate
Usually, in order to use this with your SelectElement component you will first have to convert it into a Class. You can then add the shouldComponentUpdate function to it and check the previous and next props are the same or not. If they are the same, don't update.
However, if your props are not complex objects, you can actually just recreate your SelectElement as a PureComponent. This will automatically check the props and will not re-render if they're the same.
e.g.
class SelectElement extends React.PureComponet {...
you can use memo to avoid re rendering.
Way 1:
const NestedComponent = () => {
return (
<div>
ContainerComponent
</div>
);
};
export default React.memo(NestedComponent);
Way 2:
function ParentComponent(a, b) {
const childComponent = React.useMemo(() => <ChildComponent posts={a} />, [a]);
return (
<>
{childComponent}
</>
)
}
I have this component, props are passed from parent component. Ingredients and activeIngredients are stored in the state of parent component.
export const IngredientsBox = ({
ingredients = [],
activeIngredients = [],
onAddIngredientHandler,
onRemoveIngredientHandler,
onResetIngredientsHandler
}) => {
return (
<Div>
{ingredients.map((name) => {
return (
<IngredientButton
name={name}
key={name}
isActive={activeIngredients.includes(name)}
onAddIngredientHandler={onAddIngredientHandler}
onRemoveIngredientHandler={onRemoveIngredientHandler}
/>
);
})}
<ResetButton onResetIngredientsHandler={onResetIngredientsHandler}></ResetButton>
</Div>
);
};
export const IngredientButton = ({ name, isActive, onAddIngredientHandler, onRemoveIngredientHandler }) => {
const onClick = isActive ? onRemoveIngredientHandler : onAddIngredientHandler;
return (
<Button active={isActive} onClick={() => onClick(name)}>
{name}
</Button>
);
};
What I wanna do is to test this component in isolation, but can't figure out how to imitate parent component state to change dynamically, after every method call.
import React from 'react';
import {IngredientsBox} from './IngredientsBox';
import renderer from 'react-test-renderer';
// ingredients and activeIngredients are supposed to imitate parent component state
let ingredients = ['sugar', 'honey', 'mustard', 'watermelon'];
let activeIngredients = [];
const onAddIngredientHandler = (name) => {
activeIngredients = [...activeIngredients, name]
}
const onRemoveIngredientHandler = (name) => {
activeIngredients = activeIngredients.filter((value) => value !== name)
};
test('Button toggle the class on click', () => {
const component = renderer.create(
<IngredientsBox
ingredients={ingredients}
activeIngredients = {activeIngredients}
isActive = {activeIngredients.includes(name)}
onAddIngredientHandler = {onAddIngredientHandler}
onRemoveIngredientHandler = {onRemoveIngredientHandler}
/>
);
let tree = component.toJSON();
expect(tree).toMatchSnapshot();
tree.children[0].props.onClick(); // I expect this to add this element to activeIngredients and it ofcourse works.
tree = component.toJSON();
expect(tree).toMatchSnapshot();
tree.children[0].props.onClick(); // I expect this to remove this element from activeRecipes, it doesn't work, it adds it one more time, and so on. I understand this behaviour is because onClick method was assigned at the beginning and it doesn't change.
tree = component.toJSON();
expect(tree).toMatchSnapshot()
})
Is there any way to make it behave like react component with render() method? So it rerender with fresh state each time?