I'm new to react, wanted to ask if this piece of code is good practice, because I have a feeling I'm doing something wrong but not sure what.
I have a main class component which has an array of packages which consists of width, height etc.
I'm passing this packages array as props to another functional component where I want to update these values. Currently my implementation looks like this:
<Card pck={pck} key={pck.packageId}/>
export default function Card(props) {
const widthProperties = useState(0);
props.pck.width = widthProperties[0]
const setWidth = widthProperties[1];
<input type="number" id={props.pck.packageId} className="form-control"
value={props.pck.width}
onChange={(e) => setWidth(parseInt(e.target.value))}
placeholder="Width" required/>
}
It works correctly, but as I said, I believe that I'm not using the useState with props correctly. Could someone explain what is wrong here? Because 3 lines of code to update props' state looks strange for me.
You never directly mutate props like you do here: props.pck.width = widthProperties[0].
To have a correct data flow, width and setWidth should be in the parent component and passed down to the children, so that the children can update their width by calling setWidth.
So, since the parent is a class component, you will have something like:
class CardsList extends Component {
state = {
packages: []
}
componentDidMount() {
fetch('api.com/packages')
.then(response => response.json())
.then(result => this.setState({ packages: result.items }))
}
setWidth = (width, packageId) => {
this.setState({
packages: this.state.packages.map(
pck => pck.packageId === packageId ? { ...pck, width } : pck
)
})
}
render() {
return (
<div className="cards-list">
{this.state.packages.map(pck => (
<Card pck={pck} setWidth={this.setWidth} key={pck.packageId}/>
))}
</div>
)
}
}
And a Card component like:
const Card = ({ pck, setWidth }) => (
<input value={pck.width} onChange={e => setWidth(e.target.value, pck.packageId)} />
)
It's common to destructure the value and setter function from useState like so:
[value, setValue] = useState(initialValue);
From what I gather from your question, the props.pck.width is the initial value of the input, so you may do something like this:
[width, setWidth] = useState(props.pck.width);
<input type="number" id={props.pck.packageId} className="form-control"
value={width}
onChange={(e) => setWidth(parseInt(e.target.value))}
placeholder="Width" required/>
You don't use useState like that. useState returns an array of two things:
The variable you are going to use
A function which is used to change that variable
So in you case it should look like this:
const [widthProperties, setWidthProperties] = useState({}); //Here you can either pass an empty object as an initial value or any structutre you would like.
setWidthProperties(props.pck.width); //Or whatever you want to set it to.
Remember never to change the variable manually. Do it only through the function useState gives you.
Related
I have three components First, Second and Third that need to render one after the other.
My App looks like this at the moment:
function App() {
return (
<First/>
)
}
So ideally, there's a form inside First that on submission (onSubmit probably) triggers rendering the Second component, essentially getting replaced in the DOM. The Second after some logic triggers rendering the Third component and also passes a value down to it. I'm not sure how to go on about it.
I tried using the useState hook to set a boolean state to render one of the first two components but I would need to render First, then somehow from within it change the set state in the parent which then checks the boolean and renders the second. Not sure how to do that. Something like below?
function App() {
const { isReady, setIsReady } = useState(false);
return (
isReady
? <First/> //inside this I need the state to change on form submit and propagate back up to the parent which checks the state value and renders the second?
: <Second/>
);
}
I'm mostly sure this isn't the right way to do it.
Also need to figure out how to pass the value onto another component at the time of rendering it and getting replaced in the DOM. So how does one render multiple components one after the other on interaction inside each? A button click for example?
Would greatly appreciate some guidance for this.
then somehow from within it change the set state in the parent which then checks the boolean and renders the second.
You're actually on the right track.
In React, when you're talking about UI changes, you have to manage some state.
So we got that part out of the way.
Now, what we can do in this case is manage said state in the parent component and pass functions to the children components as props in-order to allow them to control the relevant UI changes.
Example:
function App() {
const { state, setState } = useState({
isFirstVisible: true,
isSecondVisible: false,
isThirdVisible: false,
});
const onToggleSecondComponent = (status) => {
setState(prevState => ({
...prevState,
isSecondVisible: status
}))
}
const onToggleThirdComponent = (status) => {
setState(prevState => ({
...prevState,
isThirdVisible: status
}))
}
return (
{state.isFirstVisible && <First onToggleSecondComponent={onToggleSecondComponent} /> }
{state.isSecondVisible && <Second onToggleThirdComponent={onToggleThirdComponent} /> }
{state.isThirdVisible && <Third/> }
);
}
Then you can use the props in the child components.
Example usage:
function First({ onToggleSecondComponent }) {
return (
<form onSubmit={onToggleSecondComponent}
...
</form
)
}
Note that there are other ways to pass these arguments.
For example, you can have one function in the parent comp that handles them all, or you can just pass setState to the children and have them do the logic.
Either way, that's a solid way of achieving your desired outcome.
Seen as your saying there are stages, rather than having a state for each stage, just have a state for the current stage, you can then just increment the stage state to move onto the next form.
Below is a simple example, I've also used a useRef to handle parent / child state, basically just pass the state to the children and the children can update the state. On the final submit I'm just JSON.stringify the state for debug..
const FormContext = React.createContext();
const useForm = () => React.useContext(FormContext);
function FormStage1({state}) {
const [name, setName] = React.useState('');
state.name = name;
return <div>
Stage1:<br/>
name: <input value={name} onChange={e => setName(e.target.value)}/>
</div>
}
function FormStage2({state}) {
const [address, setAddress] = React.useState('');
state.address = address;
return <div>
Stage2:<br/>
address: <input value={address} onChange={e => setAddress(e.target.value)}/>
</div>
}
function FormStage3({state}) {
const [hobbies, setHobbies] = React.useState('');
state.hobbies = hobbies;
return <div>
Stage3:<br/>
hobbies: <input value={hobbies} onChange={e => setHobbies(e.target.value)}/>
</div>
}
function Form() {
const [stage, setStage] = React.useState(1);
const state = React.useRef({}).current;
let Stage;
if (stage === 1) Stage = FormStage1
else if (stage === 2) Stage = FormStage2
else if (stage === 3) Stage = FormStage3
else Stage = null;
return <form onSubmit={e => {
e.preventDefault();
setStage(s => s + 1);
}}>
{Stage
? <React.Fragment>
<Stage state={state}/>
<div>
<button>Submit</button>
</div>
</React.Fragment>
: <div>
{JSON.stringify(state)}
</div>
}
</form>
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<Form/>);
<script crossorigin src="https://unpkg.com/react#18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#18/umd/react-dom.development.js"></script>
<div id="root"></div>
Imagine two components like this in React:
import MyComponent2 from "./components/MyComponent2";
import React from "react";
export default function App() {
const [myState, setMyState] = React.useState([]);
React.useEffect(() => {
console.log("useEffect triggered");
}, [myState]);
return <MyComponent2 myState={myState} setMyState={setMyState} />;
}
import React from "react";
export default function MyComponent2(props) {
const [inputValue, setInputValue] = React.useState("");
function handleChange(e) {
setInputValue(e.target.value);
let list = props.myState;
list.push(`${e.target.value}`);
props.setMyState(list);
console.log(props.myState);
}
return (
<div>
<input
type="text"
value={inputValue}
name="text"
onChange={handleChange}
/>
</div>
);
}
As you can see I am making changes with props.setMyState line in second component. State is changing but Somehow I could not trigger React.useEffect in first component even tough It is connected with [myState]. Why ?
In short form of my question : I can not get "useEffect triggered" on my console when i make changes in input
Instead of providing myState and setMyState to MyComponent2, you should only provide setMyState and use the functional update argument in order to access the current state.
In your handleChange function you are currently mutating the React state (modifying it directly):
let list = props.myState; // This is an array that is state managed by React
list.push(`${e.target.value}`); // Here, you mutate it by appending a new element
props.setMyState(list);
// ^ You update the state with the same array here,
// and since they have the same object identity (they are the same array),
// no update occurs in the parent component
Instead, you should set the state to a new array (whose object identity differs from the current array):
props.setMyState(list => {
const newList = [...list];
newList.push(e.target.value);
return newList;
});
// A concise way to write the above is like this:
// props.setMyState(list => [...list, e.target.value]);
Component diagram:
"Main"
|--"Side"--"CategoryPicker"
|
|--"ItemBoard"
categoryPicker gets the chosen value.
const filterResultHandler = (e) => {
props.onFilterChange(e.target.value);}
...
onChange={filterResultHandler}
And lift up the value to Side.
const [filterState, setFilterState] = useState("all");
const onFilterChangeHandler = () => { props.onPassData(setFilterState);};
...
<CategoryPicker selected={filterState} onFilterChange={onFilterChangeHandler} />
Then I repeat to lift value to the Main.
(Up to this point I have console.log the value and it seemed OK.)
const [recData, setRecData] = useState("all");
const onFilterChangeHandler = (passedData) => {
setRecData(passedData);};
<Side onPassData={onFilterChangeHandler} selected={recData} />
Then pass it down to Itemboard as a prop.
<ItemBoard items={items} recData={recData} />
In ItemBoard I am trying to filter the array then compare to later map it and display filtered components.
const filteredProducts = props.items.filter((product) => {
return (product.cat.toString() === props.recData)})
{filteredProducts.map((product, index) => (
<Item cat={product.cat} />
))}
Warning: Cannot update a component (Side) while rendering a different component (Main). To locate the bad setState() call inside Main
Where am I loosing my logic?
PS.
Trying to focus on understanding how lifting up and passing props works, not looking for better solutions to the problem right now.
you have the bad setState use in this code:
const [recData, setRecData] = useState("all");
const onFilterChangeHandler = (passedData) => {
setRecData(passedData);};
<Side onPassData={onFilterChangeHandler} selected={recData} />
why you are passing this selected={recData} data again to Side component, you are updating the state of Main component from Side component and passing the selected={recData} again, remove this and try again
I've got something I don't understand here. I'm creating a hierarchy of React functional components like so:
const ContainerComponent = () => {
const [total, setTotal] = useState(initialValue);
const updateFunction = () => { console.log('It got called'); }
return (
<EntryComponent updateFunction={updateFunction} />
);
}
const EntryComponent = ({updateFunction}) => {
const services = [{}, {}, {}];
return (
{services.map((service) => <ServiceComponent updateFunction={updateFunction} />}
);
}
const ServiceComponent = ({updateFunction}) => {
return (
<input type='checkbox' onChange={updateFunction} ></input>
);
}
So the idea is that the function gets passed all the way to the ServiceComponent and attached to the checkbox for each component. On change the function should fire, running the code and updating the total value in the ContainerComponent. Problem is that this isn't happening properly and the function seems only to be passed to the first ServiceComponent instance. If I drop a console.log(updateFunction) into ServiceComponent I see the function logged for the first component, and undefined for the remaining two. This is strange even for JavaScript. Can anyone shed any light as to what is going on? As far as I understand the function should be able to be passed like any other variable, and each ServiceComponent should have it to use when needed. Indeed, if I pass a second property to the child, an object or a primitive like an integer it comes through just fine on every child component. Why is this not occurring for my function, and how do I fix it?
Edit: I see the problem, and the problem is I'm not as smart as I think I am. In the condensed version I put here everything is being supplied to the child components in the same loop, but in the actual project the child components are created in multiple places and I neglected to pass the function property to all of them. Which is mind mindbogglingly stupid on my part. I'm leaving the question here as many of you have posted replies, for which I'm grateful.
Programming is hard, I think I need a better brain.
I tried refactoring your code to this
const ContainerComponent = () => {
const [total, setTotal] = useState(0);
const updateFunction = (e) => {
console.log("update total stuff happens here");
console.log(e);
};
return <EntryComponent updateFunction={updateFunction} />;
};
const EntryComponent = ({ updateFunction }) => {
const services = ["one", "two", "three"];
return (
<>
{services.map((service) => (
<ServiceComponent updateFunction={updateFunction} />
))}
</>
);
};
const ServiceComponent = ({ updateFunction }) => (
<input type="checkbox" onChange={updateFunction}></input>
);
and it works just fine.
Try also using a react fragment in your entry component.
Below is a proof of concept pen. I'm trying to show a lot of input fields and try to collect their inputs when they change in one big object. As you can see, the input's won't change their value, which is what I expect, since they're created once with the useEffect() and filled that in that instance.
I think that the only way to solve this is to use React.cloneElement when values change and inject the new value into a cloned element. This is why I created 2000 elements in this pen, it would be a major performance hog because every element is rerendered when the state changes. I tried to use React.memo to only make the inputs with the changed value rerender, but I think cloneElement simply rerenders it anyways, which sounds like it should since it's cloned.
How can I achieve a performant update for a single field in this setup?
https://codepen.io/10uur/pen/LYPrZdg
Edit: a working pen with the cloneElement solution that I mentioned before, the noticeable performance problems and that all inputs rerender.
https://codepen.io/10uur/pen/OJLEJqM
Here is one way to achieve the desired behavior :
https://codesandbox.io/s/elastic-glade-73ivx
Some tips :
I would not recommend putting React elements in the state, prefer putting plain data (array, objects, ...) in the state that will be mapped to React elements in the return/render method.
Don't forget to use a key prop when rendering an array of elements
Use React.memo to avoid re-rendering components when the props are the same
Use React.useCallback to memoize callback (this will help when using React.memo on children)
Use the functional form of the state setter to access the old state and update it (this also helps when using React.useCallback and avoid recreating the callback when the state change)
Here is the complete code :
import React, { useEffect } from "react";
import ReactDOM from "react-dom";
import "./styles.css";
const INPUTS_COUNT = 2000;
const getInitialState = () => {
const state = [];
for (var i = 0; i < INPUTS_COUNT; i++) {
// Only put plain data in the state
state.push({
value: Math.random(),
id: "valueContainer" + i
});
}
return state;
};
const Root = () => {
const [state, setState] = React.useState([]);
useEffect(() => {
setState(getInitialState());
}, []);
// Use React.useCallback to memoize the onChangeValue callback, notice the empty array as second parameter
const onChangeValue = React.useCallback((id, value) => {
// Use the functional form of the state setter, to update the old state
// if we don't use the functional form, we will be forced to put [state] in the second parameter of React.useCallback
// in that case React.useCallback will not be very useful, because it will recreate the callback whenever the state changes
setState(state => {
return state.map(item => {
if (item.id === id) {
return { ...item, value };
}
return item;
});
});
}, []);
return (
<>
{state.map(({ id, value }) => {
// Use a key for performance boost
return (
<ValueContainer
id={id}
key={id}
onChangeValue={onChangeValue}
value={value}
/>
);
})}
</>
);
};
// Use React.memo to avoid re-rendering the component when the props are the same
const ValueContainer = React.memo(({ id, onChangeValue, value }) => {
const onChange = e => {
onChangeValue(id, e.target.value);
};
return (
<>
<br />
Rerendered: {Math.random()}
<br />
<input type="text" value={value} onChange={onChange} />
<br />
</>
);
});
ReactDOM.render(<Root />, document.getElementById("root"));