State update doesn't re-render component in ReactJS - reactjs

I have a component in which I have this useEffect:
const [charactersInfo, setCharactersInfo] = useState(null);
const [page, setPage] = useState(1);
useEffect(() => {
fetch(`https://api/api/character/?page=${page}`)
.then((res) => res.json())
.then((result) => {
setCharactersInfo(result);
});
}, [page]);
whenever my page state updates there is different data coming from the api as expected. but issue is whenever new setCharactersInfo(result) happens, it does not display the new data.
I am passing my setPage state function to this component as a prop:
<PaginationButtons
data={charactersInfo}
updatePage={(number) => {
setPage(number);
}}
/>
This is re-usable component which generates buttons and it works correctly everywhere except this specific component. any suggestions please?
import React, { useState, useEffect } from "react";
import "./PaginationButtons.css";
function PaginationButtons({ data, updatePage }) {
const [buttonsArr, setButtonsArr] = useState([]);
useEffect(() => {
const finalArray = [];
const { info } = data;
// Not the best solution for situations in which
// info.pages is big number(e.x 1000000) but since we know that
// it mostly will be 34 or less so we can just loop through it :)
for (let i = 1; i < info.pages + 1; i++) {
finalArray.push(
<button
className="page_button"
onClick={() => updatePage(i)}
key={Math.random()}
>
{i}
</button>
);
}
setButtonsArr(finalArray);
}, []);
return <div className="button_container">{buttonsArr.map((el) => el)}</div>;
}
export default PaginationButtons;
data prop is an object which contains various of stuff and on the them is the number of pages that should be displayed. in this specific case it 34 for so I use state and useEffect to loop through this number and store buttons in the state array and map it afterwards

You should handle data change in your child component as well.
pass data to useEffect dependency list.
useEffect(() => {
const finalArray = [];
const { info } = data;
// Not the best solution for situations in which
// info.pages is big number(e.x 1000000) but since we know that
// it mostly will be 34 or less so we can just loop through it :)
for (let i = 1; i < info.pages + 1; i++) {
finalArray.push(
<button
className="page_button"
onClick={() => updatePage(i)}
key={Math.random()}
>
{i}
</button>
);
}
setButtonsArr(finalArray);
}, [data]);
This should help you, no need to maintain state. and i see pages is not array its just key value pair.
function PaginationButtons({ data, updatePage }) {
const { info : { pages } } = data;
return (
<div className="button_container">
<button
className="page_button"
onClick={() => updatePage(pages || 0)}
key={pages}
>
{pages || 0}
</button>
</div>
);
}

The useEffect in PaginationButtons is using an empty dependency so it doesn't update when the data prop updates. From what I can tell you don't need the buttonsArr state anyway. It's also anti-pattern to store derived state from props. Just map the data array prop to the buttons.
Using random values is probably the least ideal method of specifying React keys. You can use the mapped array index, but you should use a unique property for each page element, or if there isn't one you should augment the data with a generated GUID.
function PaginationButtons({ data, updatePage }) {
return (
<div className="button_container">
{data.?info?.pages?.map((page, i) => (
<button
className="page_button"
onClick={() => updatePage(i)}
key={i} // <-- or page.id if available
>
{i}
</button>
))}
</div>
);
}

As stated in the other answers, you need to add data as a dependency. Also, you don't need to call map on buttonsArr, as you're not doing anything with its elements. Just use buttonsArr itself

Related

Is my usage of useEffect to generate array correct here?

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 want only one component state to be true between multiple components

I am calling components as folloews
{userAddresses.map((useraddress, index) => {
return (
<div key={index}>
<Address useraddress={useraddress} />
</div>
);
})}
Their state:
const [showEditAddress, setShowEditAddress] = useState(false);
and this is how I am handling their states
const switchEditAddress = () => {
if (showEditAddress === false) {
setShowEditAddress(true);
} else {
setShowEditAddress(false);
}
};
Well, it's better if you want to toggle between true and false to use the state inside useEffect hook in react.
useEffect will render the component every time and will get into your condition to set the state true or false.
In your case, you can try the following:
useEffect(() => { if (showEditAddress === false) {
setShowEditAddress(true);
} else {
setShowEditAddress(false);
} }, [showEditAddress])
By using useEffect you will be able to reset the boolean as your condition.
Also find the link below to react more about useEffect.
https://reactjs.org/docs/hooks-effect.html
It would be best in my opinion to keep your point of truth in the parent component and you need to figure out what the point of truth should be. If you only want one component to be editing at a time then I would just identify the address you want to edit in the parent component and go from there. It would be best if you gave each address a unique id but you can use the index as well. You could do something like the following:
UserAddress Component
const UserAddress = ({index, editIndex, setEditIndex, userAddress}) => {
return(
<div>
{userAddress}
<button onClick={() => setEditIndex(index)}>Edit</button>
{editIndex === index && <div style={{color: 'green'}}>Your editing {userAddress}</div>}
</div>
)
}
Parent Component
const UserAddresses = () => {
const addresses = ['120 n 10th st', '650 s 41 st', '4456 Birch ave']
const [editIndex, setEditIndex] = useState(null)
return userAddresses.map((userAddress, index) => <UserAddress key={index} index={index} editIndex={editIndex} setEditIndex={setEditIndex} userAddress={userAddress}/>;
}
Since you didn't post the actual components I can only give you example components but this should give you an idea of how to achieve what you want.

React - UseEffect not re-rendering with new data?

This is my React Hook:
function Student(props){
const [open, setOpen] = useState(false);
const [tags, setTags] = useState([]);
useEffect(()=>{
let input = document.getElementById(tagBar);
input.addEventListener("keyup", function(event) {
if (event.keyCode === 13) {
event.preventDefault();
document.getElementById(tagButton).click();
}
});
},[tags])
const handleClick = () => {
setOpen(!open);
};
function addTag(){
let input = document.getElementById(tagBar);
let tagList = tags;
tagList.push(input.value);
console.log("tag");
console.log(tags);
console.log("taglist");
console.log(tagList);
setTags(tagList);
}
const tagDisplay = tags.map(t => {
return <p>{t}</p>;
})
return(
<div className="tags">
<div>
{tagDisplay}
</div>
<input type='text' id={tagBar} className="tagBar" placeholder="Add a Tag"/>
<button type="submit" id={tagButton} className="hiddenButton" onClick={addTag}></button>
<div>
);
What I am looking to do is be able to add a tag to these student elements (i have multiple but each are independent of each other) and for the added tag to show up in the tag section of my display. I also need this action to be triggerable by hitting enter on the input field.
For reasons I am not sure of, I have to put the enter binding inside useEffect (probably because the input element has not yet been rendered).
Right now when I hit enter with text in the input field, it properly updates the tags/tagList variable, seen through the console.logs however, even though I set tags to be the re-rendering condition in useEffect (and the fact that it is also 1 of my states), my page is not updating with the added tags
You are correct, the element doesn't exist on first render, which is why useEffect can be handy. As to why its not re-rendering, you are passing in tags as a dependency to check for re-render. The problem is, tags is an array, which means it compares the memory reference not the contents.
var myRay = [];
var anotherRay = myRay;
var isSame = myRay === anotherRay; // TRUE
myRay.push('new value');
var isStillSame = myRay === anotherRay; // TRUE
// setTags(sameTagListWithNewElementPushed)
// React says, no change detected, same memory reference, skip
Since your add tag method is pushing new elements into the same array reference, useEffect thinks its the same array and is not re-triggers. On top of that, React will only re-render when its props change, state changes, or a forced re-render is requested. In your case, you aren't changing state. Try this:
function addTag(){
let input = document.getElementById(tagBar);
let tagList = tags;
// Create a new array reference with the same contents
// plus the new input value added at the end
setTags([...tagList, input.value]);
}
If you don't want to use useEffect I believe you can also use useRef to get access to a node when its created. Or you can put the callback directly on the node itself with onKeyDown or onKeyPress
I can find few mistake in your code. First, you attaching event listeners by yourself which is not preferred in react. From the other side if you really need to add listener to DOM inside useEffect you should also clean after you, without that, another's listeners will be added when component re-rendered.
useEffect( () => {
const handleOnKeyDown = ( e ) => { /* code */ }
const element = document.getElementById("example")
element.addEventListener( "keydown", handleOnKeyDown )
return () => element.removeEventListener( "keydown", handleOnKeyDown ) // cleaning after effect
}, [tags])
Better way of handling events with React is by use Synthetic events and components props.
const handleOnKeyDown = event => {
/* code */
}
return (
<input onKeyDown={ handleOnKeyDown } />
)
Second thing is that each React component should have unique key. Without it, React may have trouble rendering the child list correctly and rendering all of them, which can have a bad performance impact with large lists or list items with many children. Be default this key isn't set when you use map so you should take care about this by yourself.
tags.map( (tag, index) => {
return <p key={index}>{tag}</p>;
})
Third, when you trying to add tag you again querying DOM without using react syntax. Also you updating your current state basing on previous version which can causing problems because setState is asynchronous function and sometimes can not update state immediately.
const addTag = newTag => {
setState( prevState => [ ...prevState, ...newTage ] ) // when you want to update state with previous version you should pass callback which always get correct version of state as parameter
}
I hope this review can help you with understanding React.
function Student(props) {
const [tags, setTags] = useState([]);
const [inputValue, setInputValue] = useState("");
const handleOnKeyDown = (e) => {
if (e.keyCode === 13) {
e.preventDefault();
addTag();
}
};
function addTag() {
setTags((prev) => [...prev, inputValue]);
setInputValue("");
}
return (
<div className="tags">
<div>
{tags.map((tag, index) => (
<p key={index}>{tag}</p>
))}
</div>
<input
type="text"
onKeyDown={handleOnKeyDown}
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
placeholder="Add a Tag"
/>
<button type="submit" onClick={addTag}>
ADD
</button>
</div>
);
}

how can I know whether render reason is property changed or state changed in react hook function?

I have a displayer component implemented by react hook. The displayer component receives a set of records by property named 'rows'. It has two buttons used to show previous or next one, so I use a state named 'index' represents the current row no.
It is easy to show prev/next one by decrease or increase the state 'index'.
But when its rows changed, I want reset index to zero. How can I get the right condition without saving a copy of the rows to compare them?
interface row {
id: number;
name: string;
}
interface tableProps {
rows: row[];
}
const Shower: React.FC<tableProps> = (props) => {
const [index, setIndex] = useState<number>(0);
// when should I reset the index to zero when receiving a new property rows?
return <div>
<button disabled={!(index > 0)} onClick={() => setIndex(index - 1)}>Prev</button>
<span>{props.rows[index].id}:{props.rows[index].name}</span>
<button disabled={!(index + 1 < props.rows.length)} onClick={() => setIndex(index + 1)}>Prev</button>
</div>;
}
You can use useEffect
......
import {useEffect} from 'react'
....
useEffect(()=>{
setIndex(0)
},[props.rows])
I would suggest you to use useEffect (see docs) in that case:
const Shower: React.FC<tableProps> = (props) => {
const [index, setIndex] = useState<number>(0);
useEffect(() => {
setIndex(0)
},[props.rows])
// when should I reset the index to zero when receiving a new property rows?
return <div>
<button disabled={!(index > 0)} onClick={() => setIndex(index - 1)}>Prev</button>
<span>{props.rows[index].id}:{props.rows[index].name}</span>
<button disabled={!(index + 1 < props.rows.length)} onClick={() => setIndex(index + 1)}>Prev</button>
</div>;
}
What you can do is use useEffect, like:
import React, { useEffect } from 'react';
const Shower = (props) => {
const [index, setIndex] = useState<number>(0);
useEffect(() => {
setIndex(0)
}, [props.rows.length])
// ... rest of the code
}
}
on the code above, whenever the length of the rows changes it will reet the index to 0. of course you can use whatever variable and not just props.rows.length.
Just remember that the second paramteter on useEffect (inside array) will trigger the function on first parameter when those values change.

useLoopCallback -- useCallback hook for components created inside a loop

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>
);
};

Resources