I have a parent component that I want to use to dynamically add and remove an stateful component, but something weird is happening.
This is my parent component and how I control my list of FilterBlock
import React from 'react'
import FilterBlock from './components/FilterBlock'
export default function List() {
const [filterBlockList, setFilterList] = useState([FilterBlock])
const addFilterBlock = () => {
setFilterList([...filterBlockList, FilterBlock])
}
const onDeleteFilterBlock = (index) => {
const filteredList = filterBlockList.filter((block, i) => i !== index);
setFilterList(filteredList)
}
return (
<div>
{
filterBlockList.map((FilterBlock, index) => (
<FilterBlock
key={index}
onDeleteFilter={() => onDeleteFilterBlock(index)}
/>
))
}
<button onClick={addFilterBlock}></button>
</div>
)
}
You can assume FilterBlock is a stateful react hooks component.
My issue is whenever I trigger the onDeleteFilter from inside any of the added components, only the last pushed FilterBlock gets removed from the list even though I remove it based on its index on the list.
Please check my example here:
https://jsfiddle.net/emad2710/wzh5Lqj9/10/
The problem is you're storing component function FilterBlock to the state and mutate the parent state inside a child component.
You may know that mutating state directly will cause unexpected bugs. And you're mutating states here:
/* Child component */
function FilterBlock(props) {
const [value, setValue] = React.useState('')
const options = [
{ value: 'chocolate', label: 'Chocolate' },
{ value: 'strawberry', label: 'Strawberry' },
{ value: 'vanilla', label: 'Vanilla' }
]
const onInputChange = (e) => {
// child state has been changed, but parent state
// does not know about it
// thus, parent state has been mutated
setValue(e.target.value)
}
return (
<div>
<button onClick={props.onDeleteFilter}>remove</button>
<input value={value} onChange={onInputChange}></input>
</div>
)
}
/* Parent component */
const addFilterBlock = () => {
setFilterList([...filterBlockList, FilterBlock])
}
filterBlockList.map((FilterBlock, index) => (
<FilterBlock
key={index}
onDeleteFilter={() => onDeleteFilterBlock(index)}
/>
))
When a FilterBlock value changes, FilterBlock will store its state somewhere List component cannot control. That means filterBlockList state has been mutated. This leads to unpredictable bugs like the above problem.
To overcome this problem, you need to manage the whole state in parent component:
const {useCallback, useState} = React;
function FilterBlock(props) {
const options = [
{ value: 'chocolate', label: 'Chocolate' },
{ value: 'strawberry', label: 'Strawberry' },
{ value: 'vanilla', label: 'Vanilla' }
]
return (
<div>
<button onClick={props.onDeleteFilter}>remove</button>
<input value={props.value} onChange={props.onChange}></input>
</div>
)
}
function App() {
const [values, setValues] = useState(['strawberry']);
const addFilterBlock = useCallback(() => {
setValues(values.concat(''));
}, [values]);
const onDeleteFilterBlock = useCallback((index) => {
setValues(values.filter((v, i) => i !== index));
}, [values]);
const setFilterValue = useCallback((index, value) => {
const newValues = [...values];
newValues[index] = value;
setValues(newValues);
}, [values]);
return (
<div>
{
values.map((value, index) => (
<FilterBlock
key={index}
onDeleteFilter={() => onDeleteFilterBlock(index)}
value={value}
onChange={(e) => setFilterValue(index, e.target.value)}
/>
))
}
<button onClick={addFilterBlock}>add</button>
</div>
)
}
ReactDOM.render(<App />, document.querySelector("#app"))
JSFiddle
Related
I am trying to save 2 sets of input values using React Context, compare them using JSON.stringify(context1) !== JSON.stringify(context2) and if they are not the same, the page triggers a "save" CTA.
What I am not understanding is that the input updates are affecting both contexts even though I am only updating the state of one. Not understanding why this is happening.
Also, happy to hear more elegant ways of doing this.
Below is some code that replicates the issue and here is a CodePen of it:
const { useContext, useEffect, useState } = React
const Context1 = React.createContext()
const Context2 = React.createContext()
function Child(props) {
const { passHandleChange } = props
const { context1, setContext1 } = useContext(Context1)
const onChange = (index, key, event) => {
passHandleChange(index, key, event)
}
const formatPeople = () => {
return context1.tempPersons.map((person, index) => {
return (
<div>
{ index }
<input value={context1.tempPersons[index].name} onChange={event => onChange(index, 'name', event)} />
<input value={context1.tempPersons[index].email} onChange={event => onChange(index, 'email', event)} />
</div>
)
})
}
return (
<div>
{ formatPeople() }
</div>
)
}
function App() {
const initProfile = {
isModified: false,
tempPersons: [
{
name: "Bob",
email: "bob#email.com"
},
{
name: "Jill",
email: "jill#email.com"
},
{
name: "John",
email: "john#email.com"
}
]
}
const handleChange = (index, key, event) => {
let value = event.target.value
let tempPersons = context1.tempPersons
tempPersons[index] = context1.tempPersons[index]
tempPersons[index][key] = value
setContext1(prevState => ({
...prevState,
tempPersons: tempPersons
}))
}
const [context1, setContext1] = useState(initProfile)
const [context2, setContext2] = useState(initProfile.tempPersons)
return (
<div>
<Context1.Provider value={ { context1, setContext1 } }>
<Context2.Provider value={ { context2, setContext2 } }>
<Child passHandleChange={ handleChange } />
<hr />
<pre>
context1
{ JSON.stringify(context1) }
</pre>
<pre>
context2
{ JSON.stringify(context2) }
</pre>
</Context2.Provider>
</Context1.Provider>
</div>
)
}
ReactDOM.render( <App />, document.getElementById("root") );
It’s because objects in JavaScript are passed by reference, so the two contexts are actually referring to the same array (tempPersons). When that array is modified through the reference in one context, the other context will also appear modified, because they’re referring to the same array in memory.
You can confirm this by running the following:
const sharedArray = ['a', 'b'];
const ref1 = sharedArray;
const ref2 = sharedArray;
ref1.push('c');
console.log(ref2.length);
// 3
I'm using #storybook/react v6.1.21. I want to have the option to pass state to my stories using state and setState props.
This is how I defined my decorator:
//preview.js
export const decorators = [
Story => {
const [state, setState] = useState();
return <Story state={state} setState={setState} />;
}
];
// mycomponent.stories.tsx
export const TwoButtons = ({ state, setState }) => (
<ButtonGroup
buttons={[
{ label: 'One',value: 'one'},
{ label: 'Two', value: 'two' }
]}
selectedButton={state}
onClick={val => setState(val)}
/>
);
But for some reason state and setState are undefined in the story. I had a similar setup working in Storybook v5.
Any idea what i'm missing?
From the docs:
parameters.passArgsFirst: In Storybook 6+, we pass the args as the first argument to the story function. The second argument is the “context” which contains things like the story parameters etc.
You should be able to access state and setState by making a small adjustment to the story function signature:
//preview.js
export const decorators = [
Story => {
const [state, setState] = useState();
return <Story state={state} setState={setState} />;
}
];
// mycomponent.stories.tsx
export const TwoButtons = (args, context) => {
const { state, setState } = context;
return (
<ButtonGroup
buttons={[
{ label: 'One',value: 'one'},
{ label: 'Two', value: 'two' }
]}
selectedButton={state}
onClick={val => setState(val)}
/>
);
}
I've created a simple example of how useCallback is not allowing me to preserve state changes. When I remove the useCallback, the counters that I store in state update as expected, but adding useCallback (which I was hoping would keep rerenders of all speaker items to not re-render) keeps resetting my state back to the original (0,0,0).
The problem code is here in codesandbox:
https://codesandbox.io/s/flamboyant-shaw-2wtqj?file=/pages/index.js
and here is the actual simple one file example
import React, { useState, memo, useCallback } from 'react';
const Speaker = memo(({ speaker, speakerClick }) => {
console.log(speaker.id)
return (
<div>
<span
onClick={() => {
speakerClick(speaker.id);
}}
src={`speakerimages/Speaker-${speaker.id}.jpg`}
width={100}
>{speaker.id} {speaker.name}</span>
<span className="fa fa-star "> {speaker.clickCount}</span>
</div>
);
});
function SpeakerList({ speakers, setSpeakers }) {
return (
<div>
{speakers.map((speaker) => {
return (
<Speaker
speaker={speaker}
speakerClick={useCallback((id) => {
const speakersNew = speakers.map((speaker) => {
return speaker.id === id
? { ...speaker, clickCount: speaker.clickCount + 1 }
: speaker;
});
setSpeakers(speakersNew);
},[])}
key={speaker.id}
/>
);
})}
</div>
);
}
//
const App = () => {
const speakersArray = [
{ id: 1124, name: 'aaa', clickCount: 0 },
{ id: 1530, name: 'bbb', clickCount: 0 },
{ id: 10803, name: 'ccc', clickCount: 0 },
];
const [speakers, setSpeakers] = useState(speakersArray);
return (
<div>
<h1>Speaker List</h1>
<SpeakerList speakers={speakers} setSpeakers={setSpeakers}></SpeakerList>
</div>
);
};
export default App;
first, you can only use a hook at component body, you can't wrap it at speakerClick props function declaration. second, useCallback will keep the original speakers object reference, which will be a stale value. To solve this, you can use setSpeakers passing a callback instead, where your function will be called with the current speakers state:
function SpeakerList({ speakers, setSpeakers }) {
const speakerClick = useCallback(
(id) => {
// passing a callback avoid using a stale object reference
setSpeakers((speakers) => {
return speakers.map((speaker) => {
return speaker.id === id
? { ...speaker, clickCount: speaker.clickCount + 1 }
: speaker;
});
});
},
[setSpeakers] // you can add setSpeakers as dependency since it'll remain the same
);
return (
<div>
{speakers.map((speaker) => {
return (
<Speaker
speaker={speaker}
speakerClick={speakerClick}
key={speaker.id}
/>
);
})}
</div>
);
}
I have a list of checkboxes.
Checkbox is not visible as selected even after the value has been changed.
Below is my code: -
import React, { useState } from "react";
import { render } from "react-dom";
const CheckboxComponent = () => {
const [checkedList, setCheckedList] = useState([
{ id: 1, label: "First", isCheck: false },
{ id: 2, label: "Second", isCheck: true }
]);
const handleCheck = (e, index) => {
checkedList[index]["isCheck"] = e.target.checked;
setCheckedList(checkedList);
console.log(checkedList);
};
return (
<div className="container">
{checkedList.map((c, index) => (
<div>
<input
id={c.id}
type="checkbox"
checked={c.isCheck}
onChange={e => handleCheck(e, index)}
/>
<label htmlFor={c.id}>{c.label}</label>
</div>
))}
</div>
);
};
render(<CheckboxComponent />, document.getElementById("root"));
I was working fine for a simple checkbox outside the loop.
I am not sure where is the problem.
Here is the link - https://codesandbox.io/s/react-multiple-checkboxes-sczhy?file=/src/index.js:0-848
Cause you pass an array to the state, so if you want your react component re-render, you must let the react know that your state change. On your handleCheck, you only change property of an value in that array so the reference is not changed.
The handleCheck function should be look like this
const handleCheck = (e, index) => {
const newCheckList = [...checkedList];
newCheckList[index]["isCheck"] = e.target.checked;
setCheckedList(newCheckList);
};
Do this instead:
const handleCheck = (e, index) => {
setCheckedList(prevState => {
const nextState = prevState.slice()
nextState[index]["isCheck"] = e.target.checked;
return nextState
});
};
Since checkedList is an array, (considered as object and handled as such), changing a property won't change the array itself. So React can know that something changed.
I'm creating a custom steps wizard, please find the implementation below:
export const Wizard: React.FC<Props> = props => {
const {
steps,
startAtStep = 0,
showStepsNavigation = true,
prevButtonText = 'Back',
nextButtonText = 'Next',
onStepChange,
nextButtonTextOnFinalStep,
onNextClicked,
onPreviousClicked
} = props;
const [currentStep, setCurrentStep] = useState(startAtStep);
let CurrentStepComponent = steps[currentStep].Component;
const nextStep = () => {
setCurrentStep(currentStep + 1);
};
const previousStep = () => {
setCurrentStep(currentStep - 1);
};
const goToStep = (stepId: number) => {
const stepIndex = steps.findIndex(step => step.id == stepId);
if (stepIndex != -1) {
setCurrentStep(stepIndex);
}
};
const handlePreviousClick = () => {
if (onPreviousClicked && typeof onPreviousClicked == 'function') {
onPreviousClicked();
}
previousStep();
};
const handleNextClick = () => {
if (onNextClicked && typeof onNextClicked == 'function') {
onNextClicked();
}
nextStep();
};
return (
<article>
<section>
<CurrentStepComponent {...props} goToStep={goToStep} nextStep={nextStep} previousStep={previousStep} />
</section>
<footer>
<div className="wizard-buttons-container back-buttons">
<Button
className="wizard-button wizard-button--back"
secondary
onClick={handlePreviousClick}
disabled={steps[currentStep - 1] == null}
>
<i className="fas fa-chevron-left"></i>
{prevButtonText}
</Button>
</div>
<div className="wizard-buttons-container next-buttons">
<Button
className="wizard-button wizard-button--next"
onClick={handleNextClick}
disabled={steps[currentStep + 1] == null}
>
{steps[currentStep + 1] == null && nextButtonTextOnFinalStep ? nextButtonTextOnFinalStep : nextButtonText}
<i className="fas fa-chevron-right"></i>
</Button>
</div>
</footer>
</article>
);
};
The way I use it is as follows:
const steps = [
{
id: 1,
label: 'Directors and Owners',
Component: DirectorsAndOwnersStep
},
{
id: 2,
label: 'Bank Account',
Component: BankAccountStep
},
{
id: 3,
label: 'Company Documents',
Component: CompanyDocumentsStep
},
{
id: 4,
label: 'Review and Submit',
Component: ReviewAndSubmitStep
}
];
type Props = RouteComponentProps<MatchParams>;
export const EnterpriseOnboardingPage: React.FC<Props> = () => {
const onNext = () => {
console.log('Next Clicked');
};
const onPrevious = () => {
console.log('Previous Clicked');
};
return (
<section>
<Wizard
steps={steps}
nextButtonTextOnFinalStep="Submit"
onNextClicked={onNext}
onPreviousClicked={onPrevious}
/>
</section>
);
};
Now here is my problem, within the child components I want to handle what should happen when the user clicks Next, something like onNextClick execute this custom function in the child component rather than performing the default behaviour implemented in the Wizard component.
I've tried setting State in the wizard, "nextClbRegistered", and through a send the "setNextClbRegistered" to children to pass the custom function and execute it, then in the wizard in the "handleNextClick" if there is a function defined execute it. but it's always undefined.
Any ideas what is the best way to do it?
Thanks in Advance!
React is all about data flowing down in the components tree. If you want your Child to be able to show and/or modify a shared state between Child and Parent you should lift your state up and pass it down via props to it's children
const Parent = () =>{
const [title, settitle] = useState('foo')
return <Child title={title} setTitle={setTitle} />
}
const Child = ({ title, setTitle}) =>{
return <input value={title} onChange={e => setTitle(e.target.value)} />
}
In class based components
class Parent extends React.Component{
state = { title: '' }
setTitle = title => this.setState({ title })
render(){
const { title } = this.state
return <Child title={title} setTitle={this.setTitle} />
}
}
class Child extends React.Component{
render(){
const { title, setTitle } = this.props
return <input value={value} setTitle={e => setTitle(e.target.value)} />
}
}