I have a component that creates several components using a loop, but I need to rerender only the instance being modified, not the rest. This is my approach:
function renderName(item) {
return (
<TextField value={item.value || ''} onChange={edit(item.id)} />
);
}
function renderAllNames(items) {
const renderedItems = [];
items.forEach(x => {
const item = React.useMemo(() => renderName(x), [x]);
renderedItems.push(item);
});
return renderedItems;
};
return (
<>
{'Items'}
{renderAllNames(names)};
</>
);
This yells me that there are more hooks calls than in the previous render. Tried this instead:
function renderAllNames(items) {
const renderedItems = [];
items.forEach(x => {
const item = React.memo(renderName(x), (prev, next) => (prev.x === next.x));
renderedItems.push(item);
});
return renderedItems;
};
Didn't work either... the basic approach works fine
function renderAllNames(items) {
const renderedItems = [];
items.forEach(x => {
renderedItems.push(renderName(x));
});
return renderedItems;
};
But it renders all the dynamic component everytime I edit any of the fields, so how can I get this memoized in order to rerender only the item being edited?
You're breaking the rules of hooks. Hooks should only be used in the top level of a component so that React can guarantee call order. Component memoisation should also really only be done using React.memo, and components should only be declared in the global scope, not inside other components.
We could turn renderName into its own component, RenderName:
function RenderName({item, edit}) {
return (
<TextField value={item.value || ''} onChange={() => edit(item.id)} />
);
}
And memoise it like this:
const MemoRenderName = React.memo(RenderName, (prev, next) => {
const idEqual = prev.item.id === next.item.id;
const valEqual = prev.item.value === next.item.value;
const editEqual = prev.edit === next.edit;
return idEqual && valEqual && editEqual;
});
React.memo performs strict comparison on all the props by default. Since item is an object and no two objects are strictly equal, the properties must be deeply compared. A side note: this is only going to work if edit is a referentially stable function. You haven't shown it but it would have to be wrapped in a memoisation hook of its own such as useCallback or lifted out of the render cycle entirely.
Now back in the parent component you can map names directly:
return (
<>
{'Items'}
{names.map(name => <MemoRenderName item={name} edit={edit}/>)}
</>
);
Related
I'm trying to rewrite this code from class to a function. I've never used classes but I got this test code for a calendar app while learning react native but I seem to get stuck somewhere when I'm trying to replace componentDidUpdate to useEffect.
This is the old code:
export default class DaysInMonth extends React.PureComponent{
state = {
lastCalendarDayIndex: 0,
currentCalendarDayIndex: 0,
}
changeCurrentCalendarDayIndex = (index) => {
this.setState({
currentCalendarDayIndex: index
})
}
componentDidUpdate(prevProps, prevState){
if(this.state.currentCalendarDayIndex !== prevState.currentCalendarDayIndex){
this.setState({
lastCalendarDayIndex: prevState.currentCalendarDayIndex
})
}
}
render(){
return(
<>
{
this.props.row_days_array.map((rowData, index) => (
<CalendarRow
key = {'calendar row ' + index}
rowData = {rowData}
lastCalendarDayIndex = {this.state.lastCalendarDayIndex}
changeCurrentCalendarDayIndex = {this.changeCurrentCalendarDayIndex}
month_index = {this.props.month_index}
current_month_index = {this.props.current_month_index}
chooseDifferentMonth = {this.props.chooseDifferentMonth}
/>
))
}
</>
)
}
}
And this is the new code everything works except for some functions which has to do with the useEffect, I don't understand what properties I should add to get the same functionality as before. Thanks
export default function DaysInMonth({row_days_array, month_index, current_month_index, chooseDifferentMonth}) {
const [lastCalendarDayIndex, setLastCalendarDayIndex] = useState(0)
const [currentCalendarDayIndex, setCurrentCalendarDayIndex] = useState(0)
const changeCurrentCalendarDayIndex = (index) => {
setCurrentCalendarDayIndex(index)
}
useEffect(() => {
if(currentCalendarDayIndex !== currentCalendarDayIndex){
setLastCalendarDayIndex(currentCalendarDayIndex)
}
},[]);
return (
<>
{row_days_array.map((rowData, index) => (
<CalendarRow
key = {'calendar row ' + index}
rowData = {rowData}
lastCalendarDayIndex = {lastCalendarDayIndex}
changeCurrentCalendarDayIndex = {changeCurrentCalendarDayIndex}
month_index = {month_index}
current_month_index = {current_month_index}
chooseDifferentMonth = {chooseDifferentMonth}
/>
))}
</>
)
}
What this does:
useEffect(() => {
if(currentCalendarDayIndex !== currentCalendarDayIndex){
setLastCalendarDayIndex(currentCalendarDayIndex)
}
},[]);
It runs the code inside useEffect every time the array changes. Because it's an empty array it will just run once (once the component mounts, this is basically the old ComponentDidMount), to mimic the behaviour of ComponentDidUpdate you need to keep track of the props so it should be a matter of passing them into the array (so React can track when it changes):
useEffect(() => {
if(currentCalendarDayIndex !== currentCalendarDayIndex){
setLastCalendarDayIndex(currentCalendarDayIndex)
}
},[props]);
It's probably easier to change the destructuring you have in the component definition {row_days_array, month_index, current_month_index, chooseDifferentMonth} to props and then destructure a bit bellow, so you can use the whole object in you useEffect array
You can pass a callback in setState to access previous state, and pass currentCalendarDayIndex in useEffect dependency to update lastedCalendarState every currentCalendar changes. Hope this can help!
useEffect(() => {
setLastCalendarDayIndex((prevCalendarDayIndex) =>
prevCalendarDayIndex !== currentCalendarDayIndex
? currentCalendarDayIndex
: prevCalendarDayIndex)
}, [currentCalendarDayIndex])
I want to generate a 16-length array of random prizes using prizes array that is passed as a prop in Board component, and display them.
prizes array -
[
{
prizeId: 1,
name: 'coupon',
image: 'img/coupon.svg',
},
{
prizeId: 2,
name: 'gift card',
image: 'img/gift-card.svg',
},
// more prizes
]
In Board.js -
const Board = ({ prizes }) => {
const [shuffledPrizes, setShuffledPrizes] = useState(null)
useEffect(() => {
setShuffledPrizes(shuffleArray(populatePrize(16, prizes)))
}, [prizes])
return (
<div>
{
shuffledPrizes && shuffledPrizes.map((prize) => (
<Prize
key={prize.id}
prize={prize}
/>
))
}
</div>
)
}
In populatePrize function, I have to add id to use as React key because already existed prizeId can't be used, as prizes will be duplicated -
import { nanoid } from 'nanoid'
const populatePrize = (noOfBlock, prizeArray) => {
const arrayToPopulate = []
let index = 0
for (let i = 0; i < noOfBlock; i += 1, index += 1) {
if (index === prizeArray.length) {
index = 0
}
arrayToPopulate.push({
id: nanoid(),
prizeId: prizeArray[index].prizeId,
name: prizeArray[index].name,
image: prizeArray[index].image,
})
}
return arrayToPopulate
}
Is using useState and useEffect necessary here? Because, I don't think generating an array and shuffling it is a side effect, and I can just use a variable outside of Board function like -
let shuffledPrizes = null
const Board = ({ prizes }) => {
if (!shuffledPrizes)
shuffledPrizes = shuffleArray(populatePrize(16, prizes))
}
return (
<div>
{
shuffledPrizes.map((prize) => (
<Prize
key={prize.id}
prize={prize}
/>
))
}
</div>
)
}
But, with that way, every <Board /> component references and display the same shuffledPrizes array, not randomly for each Board component like I want.
Reusing Board is not a requirement, but I read in React docs about components being pure functions and I don't think mine is one. I am also confused in when to use a variable outside or inside of a component, and when to use state.
Although my question might be about using useEffect, I want to learn how to improve this code in proper React way.
This in indeed not a good use case of useEffect.
Effects are an escape hatch from the React paradigm. They let you
“step outside” of React and synchronize your components with some
external system like a non-React widget, network, or the browser DOM.
If there is no external system involved (for example, if you want to
update a component’s state when some props or state change), you
shouldn’t need an Effect. Removing unnecessary Effects will make your
code easier to follow, faster to run, and less error-prone.
You can shuffle the array when you pass it trough props.
const BoardContainer = () => <div>
<Board prizes={shuffleArray(populatePrize(16, prices))}/>
<Board prizes={shuffleArray(populatePrize(16, prices))}/>
</div>
You can also use the lazy version of useState that is only evaluated during the first render
const Board = ({prizes}) => {
const [shuffledPrizes,] = useState(() => shuffleArray(populatePrize(16, prizes)))
return (
<div>
<ul>
{
shuffledPrizes && shuffledPrizes.map((prize) => (
<Prize
key={prize.id}
prize={prize}
/>
))
}
</ul>
</div>
)
}
Your prizes are given in props, so they can potentially be updated ? By a fetch or something like that.
In that case, you can :
cont defaultArray = []; // avoid to trigger useEffect at each update with a new array in initialization
const Board = ({ prizes = defaultArray }) => {
const [shuffledPrizes, setShuffledPrizes] = useState([])
useEffect(() => {
if(prizes.length) {
setShuffledPrizes(shuffleArray(populatePrize(16, prizes)));
}
}, [prizes]);
return (
<div>
{
shuffledPrizes.map((prize) => (
<Prize
key={prize.id}
prize={prize}
/>
))
}
</div>
)
}
If you do :
const Board = ({ prizes }) => {
const shuffledPrizes = shuffleArray(populatePrize(16, prizes))
return (
<div>
{
shuffledPrizes.map((prize) => (
<Prize
key={prize.id}
prize={prize}
/>
))
}
</div>
)
}
populatePrize and shuffleArray will be called at each render. Maybe it could works if your only props is prices and you use React.memo. But it's harder to maintain, I think.
Making a variable out of your component like that, will not let your component listen to this variable modifications. You can do this for constants.
Each render you test !shuffledPrizes so when it will be filled once, your variable will be filled too and your component will render correctly. But if you change prizes, shuffledPrizes will not be updated. It's not a good practice.
With a different condition, you can continue to update your out component variable listening to prop changes that trigger a render. But useEffect is the better way to listen if your prop changes.
In the code you post, shuffledPrizes can be null, so you should put a condition before calling .map()
My self, I would call the suffle function in the parent that store it in is state, to store it directly with shuffling and not calling shuffle function at a wrong rerender.
I have a react functional component that accesses the MobX store with useContext. I have found two ways to observe an array that is an observable from the store. First, the useObserver hook and wrapping the component with observer.
I thought that these are the same but that the useObserver only observes specific properties (such as the array that is passed) but I am experiencing a problem when the array reaches size 2 and then the component does not re-render. That's the case when using useObserver. When wrapping with observer, this is fixed.
Can anyone explain why this is happening and what's the difference?
const ApplesContainer = observer(() => {
const stores = useStores();
const applesArray = stores.fruits.apples;
return (
{applesArray.map(apple => (
<Apple key={apple.id} apple={apple} />
))}
);
});
// OR with useObserver()
function useGlobalState() {
const stores = useStores();
return useObserver(() => ({
applesArray: stores.fruits.apples
}));
}
const ApplesContainer = observer(() => {
const { applesArray } = useGlobalState();
return (
{applesArray.map(apple => (
<Apple key={apple.id} apple={apple} />
))}
);
});
useObserver must return JSX with an observable value.
This hook takes care of tracking changes and re-rendering them.
If no observable value exists in JSX, then it won't be re-rendered.
e.g.:
const SomeContainer =() => {
const { someStores } = useStores();
return useObserver(()=>(
{someStore.data.map(val => (
<Apple key={val.id} val={val} />
))}
));
};
Suppose I have a component that renders a list item:
const ListItem = ({ itemName }) => {
return (
<div>
{itemName}
</div>
)
}
And because this list item is used in many places in my app, I define a custom hook to render the list and control the behavior of each list instance:
const useListItems = () => {
const [ showList, setShowList ] = useState(true)
const { listItemArray, isLoaded } = useListContext() // Context makes api call
const toggleShowList = setShowList(!showList)
function renderListItems() {
return isLoaded && !!listItemArray ? listItemArray.map((itemName, index) => (
<ListItem key={index} isVisible={showList} itemName={itemName}/>
))
:
null
}
// Some other components and logic...
return {
// ...Other components and logic,
renderListItems,
toggleShowList,
}
}
My first question is, when will the array of ListItems actually be evaluated ? Will the jsx resulting from renderListItems() be calculated every time renderListItems() is called? Or would it happen every time useListItems() is called?
Second question: if I call useListItems() in another component but don't call renderListItems(), does that impact whether the components are evaluated?
I have been struggling to find an answer to this, so thanks in advance.
I'd like to start a discussion on the recommended approach for creating callbacks that take in a parameter from a component created inside a loop.
For example, if I'm populating a list of items that will have a "Delete" button, I want the "onDeleteItem" callback to know the index of the item to delete. So something like this:
const onDeleteItem = useCallback(index => () => {
setList(list.slice(0, index).concat(list.slice(index + 1)));
}, [list]);
return (
<div>
{list.map((item, index) =>
<div>
<span>{item}</span>
<button type="button" onClick={onDeleteItem(index)}>Delete</button>
</div>
)}
</div>
);
But the problem with this is that onDeleteItem will always return a new function to the onClick handler, causing the button to be re-rendered, even when the list hasn't changed. So it defeats the purpose of useCallback.
I came up with my own hook, which I called useLoopCallback, that solves the problem by memoizing the main callback along with a Map of loop params to their own callback:
import React, {useCallback, useMemo} from "react";
export function useLoopCallback(code, dependencies) {
const callback = useCallback(code, dependencies);
const loopCallbacks = useMemo(() => ({map: new Map(), callback}), [callback]);
return useCallback(loopParam => {
let loopCallback = loopCallbacks.map.get(loopParam);
if (!loopCallback) {
loopCallback = (...otherParams) => loopCallbacks.callback(loopParam, ...otherParams);
loopCallbacks.map.set(loopParam, loopCallback);
}
return loopCallback;
}, [callback]);
}
So now the above handler looks like this:
const onDeleteItem = useLoopCallback(index => {
setList(list.slice(0, index).concat(list.slice(index + 1)));
}, [list]);
This works fine but now I'm wondering if this extra logic is really making things faster or just adding unnecessary overhead. Can anyone please provide some insight?
EDIT:
An alternative to the above is to wrap the list items inside their own component. So something like this:
function ListItem({key, item, onDeleteItem}) {
const onDelete = useCallback(() => {
onDeleteItem(key);
}, [onDeleteItem, key]);
return (
<div>
<span>{item}</span>
<button type="button" onClick={onDelete}>Delete</button>
</div>
);
}
export default function List(...) {
...
const onDeleteItem = useCallback(index => {
setList(list.slice(0, index).concat(list.slice(index + 1)));
}, [list]);
return (
<div>
{list.map((item, index) =>
<ListItem key={index} item={item} onDeleteItem={onDeleteItem} />
)}
</div>
);
}
Performance optimizations always come with a cost. Sometimes this cost is lower than the operation to be optimized, sometimes is higher. useCallback it's a hook very similar to useMemo, actually you can think of it as a specialization of useMemo that can only be used in functions. For example, the bellow statements are equivalents
const callback = value => value * 2
const memoizedCb = useCallback(callback, [])
const memoizedWithUseMemo = useMemo(() => callback, [])
So for now on every assertion about useCallback can be applied to useMemo.
The gist of memoization is to keep copies of old values to return in the event we get the same dependencies, this can be great when you have something that is expensive to compute. Take a look at the following code
const Component = ({ items }) =>{
const array = items.map(x => x*2)
}
Uppon every render the const array will be created as a result of a map performed in items. So you can feel tempted to do the following
const Component = ({ items }) =>{
const array = useMemo(() => items.map(x => x*2), [items])
}
Now items.map(x => x*2) will only be executed when items change, but is it worth? The short answer is no. The performance gained by doing this is trivial and sometimes will be more expensive to use memoization than just execute the function each render. Both hooks(useCallback and useMemo) are useful in two distinct use cases:
Referencial equality
When you need to ensure that a reference type will not trigger a re render just for failing a shallow comparison
Computationally expensive operations(only useMemo)
Something like this
const serializedValue = {item: props.item.map(x => ({...x, override: x ? y : z}))}
Now you have a reason to memoized the operation and lazily retrieve the serializedValue everytime props.item changes:
const serializedValue = useMemo(() => ({item: props.item.map(x => ({...x, override: x ? y : z}))}), [props.item])
Any other use case is almost always worth to just re compute all values again, React it's pretty efficient and aditional renders almost never cause performance issues. Keep in mind that sometimes your efforts to optimize your code can go the other way and generate a lot of extra/unecessary code, that won't generate so much benefits (sometimes will only cause more problems).
The List component manages it's own state (list) the delete functions depends on this list being available in it's closure. So when the list changes the delete function must change.
With redux this would not be a problem because deleting items would be accomplished by dispatching an action and will be changed by a reducer that is always the same function.
React happens to have a useReducer hook that you can use:
import React, { useMemo, useReducer, memo } from 'react';
const Item = props => {
//calling remove will dispatch {type:'REMOVE', payload:{id}}
//no arguments are needed
const { remove } = props;
console.log('component render', props);
return (
<div>
<div>{JSON.stringify(props)}</div>
<div>
<button onClick={remove}>REMOVE</button>
</div>
</div>
);
};
//wrap in React.memo so when props don't change
// the ItemContainer will not re render (pure component)
const ItemContainer = memo(props => {
console.log('in the item container');
//dispatch passed by parent use it to dispatch an action
const { dispatch, id } = props;
const remove = () =>
dispatch({
type: 'REMOVE',
payload: { id },
});
return <Item {...props} remove={remove} />;
});
const initialState = [{ id: 1 }, { id: 2 }, { id: 3 }];
//Reducer is static it doesn't need list to be in it's
// scope through closure
const reducer = (state, action) => {
if (action.type === 'REMOVE') {
//remove the id from the list
return state.filter(
item => item.id !== action.payload.id
);
}
return state;
};
export default () => {
//initialize state and reducer
const [list, dispatch] = useReducer(
reducer,
initialState
);
console.log('parent render', list);
return (
<div>
{list.map(({ id }) => (
<ItemContainer
key={id}
id={id}
dispatch={dispatch}
/>
))}
</div>
);
};