I want only one component state to be true between multiple components - reactjs

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.

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.

React Hooks "useState/useEffect/useCallback" are called conditionally

Please tell me where do I need to put the list.length condition to remove the React Hooks are called conditionally error? I tried to wrap it in useEffect, but in this case an empty list is returned at the first render. It is important that the list is returned at the first render in the same way as with the logic in the code below.
const List = ({ list }) => {
if (list.length === 0) {
return <div>LOADING...</div>;
}
const [localList, setLocalList] = useState(list);
useEffect(() => {
setList(localList);
}, [localList]);
const handleChange = useCallback((id) => {
setLocalList((prevLocalList) =>
prevLocalList.map((item, index) => {
return index !== id ? item : { ...item, checked: !item.checked };
})
);
}, []);
return (
<>
{localList?.map((item, index) => (
<MemoRow key={index} {...item} handleChange={handleChange} />
))}
</>
);
};
The rendered result is returned at the end of the component, not at the beginning. Make that first operation part of the overall return at the end:
return (
list.length === 0 ?
<div>LOADING...</div> :
<>
{localList?.map((item, index) => (
<MemoRow key={index} {...item} handleChange={handleChange} />
))}
</>
);
Additionally, there is a logical issue in your component. When a parent component passes the list value, you are duplicating that in local state in this component. If the parent component changes the value of list, this component will re-render but will not update its local state.
Given the term "LOADING..." in the UI, this implies that's exactly what's happening here. So on a re-render, list.length === 0 is now false, but localList is still empty.
As a "quick fix" you can just update localList any time list changes:
useEffect(() => {
setLocalList(list);
}, [list, setLocalList]);
Of course, this will also over-write any local changes to localList if the parent component ever changes list again. But since this is duplicated state then it's not really clear what should happen in that case anyway. Perhaps you could only conditionally update it if localList is empty:
useEffect(() => {
if (localList.length === 0) {
setLocalList(list);
}
}, [list, setLocalList, localList]);
It's really up to you how you want to handle edge cases like that. But ultimately you're going to need to update localList after list has changed if you want those changes to be reflected in your local state.

State update doesn't re-render component in 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

How to render a different component with React Hooks

I have a parent component with an if statement to show 2 different types of buttons.
What I do, on page load, I check if the API returns an array called lectures as empty or with any values:
lectures.length > 0 ? show button A : show button B
This is the component, called main.js, where the if statement is:
lectures.length > 0
? <div onClick={() => handleCollapseClick()}>
<SectionCollapse open={open} />
</div>
: <LectureAdd dataSection={dataSection} />
The component LectureAdd displays a + sign, which will open a modal to create a new Lecture's title, while, SectionCollapse will show an arrow to show/hide a list of items.
The logic is simple:
1. On page load, if the lectures.lenght > 0 is false, we show the + sign to add a new lecture
OR
2. If the lectures.lenght > 0 is true, we change and show the collpase arrow.
Now, my issue happens when I add the new lecture from the child component LectureAdd.js
import React from 'react';
import { Form, Field } from 'react-final-form';
// Constants
import { URLS } from '../../../../constants';
// Helpers & Utils
import api from '../../../../helpers/API';
// Material UI Icons
import AddBoxIcon from '#material-ui/icons/AddBox';
export default ({ s }) => {
const [open, setOpen] = React.useState(false);
const [ lucturesData, setLecturesData ] = React.useState(0);
const { t } = useTranslation();
const handleAddLecture = ({ lecture_title }) => {
const data = {
"lecture": {
"title": lecture_title
}
}
return api
.post(URLS.NEW_COURSE_LECTURE(s.id), data)
.then(data => {
if(data.status === 201) {
setLecturesData(lucturesData + 1) <=== this doesn't trigger the parent and the button remains a `+` symbol, instead of changing because now `lectures.length` is 1
}
})
.catch(response => {
console.log(response)
});
}
return (
<>
<Button variant="outlined" color="primary" onClick={handleClickOpen}>
<AddBoxIcon />
</Button>
<Form
onSubmit={event => handleAddLecture(event)}
>
{
({
handleSubmit
}) => (
<form onSubmit={handleSubmit}>
<Field
name='lecture_title'
>
{({ input, meta }) => (
<div className={meta.active ? 'active' : ''}>
<input {...input}
type='text'
className="signup-field-input"
/>
</div>
)}
</Field>
<Button
variant="contained"
color="primary"
type="submit"
>
ADD LECTURE
</Button>
</form>
)}
</Form>
</>
)
}
I've been trying to use UseEffect to trigger a re-render on the update of the variable called lucturesData, but it doesn't re-render the parent component.
Any idea?
Thanks Joe
Common problem in React. Sending data top-down is easy, we just pass props. Passing information back up from children components, not as easy. Couple of solutions.
Use a callback (Observer pattern)
Parent passes a prop to the child that is a function. Child invokes the function when something meaningful happens. Parent can then do something when the function gets called like force a re-render.
function Parent(props) {
const [lectures, setLectures] = useState([]);
const handleLectureCreated = useCallback((lecture) => {
// Force a re-render by calling setState
setLectures([...lectures, lecture]);
}, []);
return (
<Child onLectureCreated={handleLectureCreated} />
)
}
function Child({ onLectureCreated }) {
const handleClick = useCallback(() => {
// Call API
let lecture = callApi();
// Notify parent of event
onLectureCreated(lecture);
}, [onLectureCreated]);
return (
<button onClick={handleClick}>Create Lecture</button>
)
}
Similar to solution #1, except for Parent handles API call. The benefit of this, is the Child component becomes more reusable since its "dumbed down".
function Parent(props) {
const [lectures, setLectures] = useState([]);
const handleLectureCreated = useCallback((data) => {
// Call API
let lecture = callApi(data);
// Force a re-render by calling setState
setLectures([...lectures, lecture]);
}, []);
return (
<Child onLectureCreated={handleLectureCreated} />
)
}
function Child({ onLectureCreated }) {
const handleClick = useCallback(() => {
// Create lecture data to send to callback
let lecture = {
formData1: '',
formData2: ''
}
// Notify parent of event
onCreateLecture(lecture);
}, [onCreateLecture]);
return (
<button onClick={handleClick}>Create Lecture</button>
)
}
Use a central state management tool like Redux. This solution allows any component to "listen in" on changes to data, like new Lectures. I won't provide an example here because it's quite in depth.
Essentially all of these solutions involve the same solution executed slightly differently. The first, uses a smart child that notifies its parent of events once their complete. The second, uses dumb children to gather data and notify the parent to take action on said data. The third, uses a centralized state management system.

useRef in a dynamic context, where the amount of refs is not constant but based on a property

In my application I have a list of "chips" (per material-ui), and on clicking the delete button a delete action should be taken. The action needs to be given a reference to the chip not the button.
A naive (and wrong) implementation would look like:
function MemberList(props) {
const {userList} = this.props;
refs = {}
for (const usr.id of userList) {
refs[usr.id] = React.useRef();
}
return <>
<div >
{
userList.map(usr => {
return <UserThumbView
ref={refs[usr.id]}
key={usr.id}
user={usr}
handleDelete={(e) => {
onRemove(usr, refs[usr.id])
}}
/>
}) :
}
</div>
</>
}
However as said this is wrong, since react expects all hooks to always in the same order, and (hence) always be of the same amount. (above would actually work, until we add a state/any other hook below the for loop).
How would this be solved? Or is this the limit of functional components?
Refs are just a way to save a reference between renders. Just remember to check if it is defined before you use it. See the example code below.
function MemberList(props) {
const refs = React.useRef({});
return (
<div>
{props.userList.map(user => (
<UserThumbView
handleDelete={(e) => onRemove(user, refs[user.id])}
ref={el => refs.current[user.id] = el}
key={user.id}
user={user}
/>
})}
</div>
)
}

Resources