Please reference this Codesandbox for my example.
I am having problems deleting components that get rendered via a function. Since components should not be stored in state, I store their type in an array in the parent component's state instead. A function iterates through that array to render the components. When I attempt to delete a specific component by index, React does not update the props (the index) in the component I want to keep (ie: changing the component at index 2 down to index 1). Instead, it renders the component already at that index. My Codesandbox example demonstrates this behavior.
How can I delete a component that is rendered in this way (via a function that uses the parent's state to populate props)? Is there another way I could be rendering these components?
Like #MichaelRovinsky said:
Give each component a unique identifier. When you delete a component,
find it by id and remove from the array. Don't use index because it's
not persistent
So, you need to use a unique identifier.
For that, you can use react-uuid.
First, I've redefined your step as:
export class Step {
Id;
Type;
Value;
}
So, your default data should be like:
const defaultSteps = [
{ Type: ALPHA, Id: uuid(), Value: 0 },
{ Type: ALPHA, Id: uuid(), Value: 0 },
{ Type: ALPHA, Id: uuid(), Value: 0 }
];
I've also changed Alpha component's index prop with id.
Inside your renderSteps function:
case ALPHA:
return (
<Alpha
id={step.Id}
onChange={onValueChange}
onDelete={index > 0 ? () => onDelete(step) : null}
key={step.Id}
/>
);
See, now both the id prop and key are the same and unique.
onValueChange event:
const onValueChange = (id, newValue) => {
let currentSteps = [...steps];
const step = currentSteps.filter((f) => f.Id === id)[0];
step.Value = newValue;
setSteps(currentSteps);
setValues(currentSteps.map((m) => m.Value));
};
So, setting your values becomes much simpler without tracing indexes.
onDelete event:
const onDelete = (step) => {
let currentSteps = [...steps];
const index = currentSteps.indexOf(step);
currentSteps.splice(index, 1);
setSteps(currentSteps);
setValues(currentSteps.map((m) => m.Value));
};
As all of your required properties are now inside single object, it's much easier to handle both steps and values state.
Working demo at CodeSandbox.
It is kind of hard for me to make sense of your code but the live demo looks clear enough.
Your state is an array and each item can be rendered as an isolated component. What you want to do is let the state flow down into your components, and wire them up to update it properly (when you click the delete key, an array item needs to be removed).
Here's how you can achieve this in a much simpler way: CodeSandbox demo.
Related
(sory for my english)
Hi, this code working half to half. I can delete product but i must go to another page , and back , to rerender. i dont know why ?
Code:
Parent Component:
function ShoppingCard() {
const shoppingCard = useContext<any>(ShoppingCardContext);
const [shoppingCard2, setShoppingCard2] = useState<any>(shoppingCard);
useEffect(() => {
setShoppingCard2(shoppingCard);
}, [shoppingCard, shoppingCard2]);
const removeProduct = (shopCart: any) => {
let index = shoppingCard.findIndex(
(oneProduct: any) => oneProduct.idProduct === shopCart.idProduct
);
shoppingCard.splice(index, 1);
setShoppingCard2(shoppingCard);
};
// passes this function to the next component through the next component.
Parent component 1
child component 2
child Component 3
And i use this fuction in a button to remove element. In console i see that shoppingCard and shoppingCard2 is update but only in child 3. And to see the efect (rerender) i must go to another page and back. In app i use React-Router.
Ps. This is my first post
Issue
You are mutating the state when deleting from the shoppingCard2 array, and then never creating a new array reference for React so reconciliation works. In other words, React doesn't "see" that the shoppingCard2 array is a new array so it doesn't trigger a rerender. You see the mutation when navigating away and back to the page.
const removeProduct = (shopCart: any) => {
let index = shoppingCard.findIndex(
(oneProduct: any) => oneProduct.idProduct === shopCart.idProduct
);
shoppingCard.splice(index, 1); // <-- mutation!!
setShoppingCard2(shoppingCard); // <-- same array reference
};
Solution
Shallow copy the shoppingCard2 array and remove the element. Use Array.prototype.filter to accomplish this.
Example:
const removeProduct = (shopCart: any) => {
setShoppingCard2(shoppingCard => shoppingCard.filter(
(oneProduct: any) => oneProduct.idProduct !== shopCart.idProduct
));
};
Suggestion
An improvement would be to expose a state updater function from the ShoppingCardContext so it can maintain the state invariant instead of offloading this to consumers. There's really no reason to "duplicate" the shopping card state locally if it can be helped.
The best will be to use a global state. You can read more about
React Context
Redux
another state manager
I am trying to update my state data based on the users input in two fields and I'm not sure if Im going about it the right way.
The parent component Encounter.js holds the state I will try and limit the amount of code I add here so my issue is clear. So in ComponentDidUpdate I set the state with an object and create an update function to update the state. I pass the two values inside my state to another component PatientInfo along with the update state function:
componentDidUpdate(prevProps) {
if (this.props.details && this.props.details.medicalIntake && !prevProps.details.medicalIntake) {
this.setState({ pertinentMedications: {
covid19Protocol: this.props.details.medicalIntake.pertinentMedications.covid19Protocol,
note: "" || this.props.details.medicalIntake.pertinentMedications.notes
}})
}
}
pertinentMedicationsChange = (newValues) => {
this.props.setIdleTime();
this.props.setState({pertinentMedications: newValues});
}
return (
<PatientInfo
covid19Protocol={this.state.pertinentMedications.covid19Protocol}
pertinentMedicationsNote={this.state.pertinentMedications.note}
pertinentMedicationsChange={this.pertinentMedicationsChange}
/>
)
PatientInfo.js simply passes the props down.
<PertinentMedications
covid19Protocol={this.props.covid19Protocol}
pertinentMedicationsNote={this.props.pertinentMdicationsNote}
pertinentMedicationsChange={this.props.pertinentMedicationsChange}
/>
PertinentMedications.js is where the user input will be collected:
const PertinentMedications = ({
covid19Protocol,
pertinentMedicationsNote,
pertinentMedicationsChange
}) => {
const [isChecked, setIsChecked] = useState(covid19Protocol)
const onClick = (field, value) => {
setIsChecked(!isChecked)
pertinentMedicationsChange( {[field]: value})
}
const onNoteChange = (field, value) => {
pertinentMedicationsChange( {[field]: value})
}
return(
<ContentBlock title="Pertinent Medications and Supplements">
<CheckToggle onChange={() => onClick("covid19Protocol", !covid19Protocol)} checked={isChecked}>
<p>Patient has been receiving the standard supportive care and supplements as per COVID-19 protocol.</p>
</CheckToggle>
<Input
type="textarea"
name="pertinentMedications"
onChange={e => onNoteChange("notes" ,e.target.value)}
value={pertinentMedicationsNote}
/>
</ContentBlock>
)
}
export default PertinentMedications;
My true question lies within the pertinentMedicationsChange function as Im not sure how to take the data im getting from the PertinentMedications component and format it to be placed in the state. First Im not sure if I can update the state the way im trying to with these two independent fields that send their data to this function to change the state? And If it is possible Im not sure how to properly setup the key value pairs when i call setState. Can anyone help?
it seems that you are calling this.props.setState instead of this.setState. Second, this.setState also accepts a function which first param is the previous state. In this way you can use it to prevent its key values saved from pertinentMedications to be overwritten. fwiw, it's better to be consistent, not mixing hooks with react component based.
pertinentMedicationsChange = (newValues) => {
this.props.setIdleTime();
this.setState((state) => ({
// you create a new object with previous values, while newValues updates the proper keys, but not removing other keys
pertinentMedications: { ...state.pertinentMedications,...newValues}
});
)};
I thought that context acts in a similar way as state so I've created function handleUpdate that can update state which is context using as a value. Afterwards I've noticed that context is being updated without triggering handleUpdate.
Provider:
<DashboardContext.Provider value={{dashboard:this.state,handleChange:this.handleChange}}>
{/*...*/}
</DashboardContext.Provider>
handleChange function
handleChange=(what, value)=> this.setState({[what]:value});
In another component which uses context: this triggers updating of context without calling handleUpdate.
let updatedTasks = this.context.dashboard.tasks;
updatedTasks[task.id] = {/* ... something */};
Afterwards it changes context value and parents state (which is context using) without calling setState. Is this usual behavior? I though that all states should be handled with setState function.
As the actual workaround to lose reference on contexts object I've used:
let updatedTasks = JSON.parse(JSON.stringify(this.context.dashboard.tasks));
but it doesn't seems like a correct solution for me.
Edit: as #Nicholas Tower suggested solution:
my current code
State in constructor now looks like this:
this.state = {
value: {
dashboard: {
// everything from state is now here
},
handleChange: this.handleChange,
}
};
I pass state.value instead of custom object now
<DashboardContext.Provider value={this.state.value}>
{/*...*/}
</DashboardContext.Provider>
but still when I do this, context and state (both) are being updated without calling handleChange
let updatedTasks = this.context.dashboard.tasks;
updatedTasks[task.id] = {/* ... something */};
The issue you have is in this part:
value={{dashboard:this.state,handleChange:this.handleChange}}
Every time your component renders (for whatever reason), this line will create a new object. The dashboard property and handleChange property may be unchanged, but the object surrounding them is always new. That's enough that every time it renders, the value changes, and so all descendants that use the value need to be rerendered too.
You'll need to modify your code so that this object reference does not change unless you want it to. This is typically done by putting the object into the component's state.
class Example {
handleChange = (what, value) => {
this.setState(oldState => ({
value: {
... oldState.value,
[what]:value
}
});
}
state = {
value: {
dashboard: {
// whatever you used to put in state now goes here
},
handleChange: this.handleChange
}
}
render() {
return (
<DashboardContext.Provider value={this.state.value}>
{/*...*/}
</DashboardContext.Provider>
)
}
}
You can see mention of this in React's documentation on context: https://reactjs.org/docs/context.html#caveats
What is the best way to share some global values and functions in react?
Now i have one ContextProvider with all of them inside:
<AllContext.Provider
value={{
setProfile, // second function that changes profile object using useState to false or updated value
profileReload, // function that triggers fetch profile object from server
deviceTheme, // object
setDeviceTheme, // second function that changes theme object using useState to false or updated value
clickEvent, // click event
usePopup, // second function of useState that trigers some popup
popup, // Just pass this to usePopup component
windowSize, // manyUpdates on resize (like 30 a sec, but maybe can debounce)
windowScroll // manyUpdates on resize (like 30 a sec, but maybe can debounce)
}}
>
But like sad in docs:
Because context uses reference identity to determine when to re-render, there are some gotchas that could trigger unintentional renders in consumers when a provider’s parent re-renders. For example, the code below will re-render all consumers every time the Provider re-renders because a new object is always created for value:
This is bad:
<Provider value={{something: 'something'}}>
This is ok:
this.state = {
value: {something: 'something'},
};
<Provider value={this.state.value}>
I imagine that in future i will have maybe up to 30 context providers and it's not very friendly :/
So how can i pass this global values and functions to components? I can just
Create separate contextProvider for everything.
Group something that used together like profile and it's functions,
theme and it's functions (what about reference identity than?)
Maybe group only functions because thay dont change itself? what
about reference identity than?)
Other simpliest way?
Examples of what i use in Provider:
// Resize
const [windowSize, windowSizeSet] = useState({
innerWidth: window.innerWidth,
innerHeight: window.innerHeight
})
// profileReload
const profileReload = async () => {
let profileData = await fetch('/profile')
profileData = await profileData.json()
if (profileData.error)
return usePopup({ type: 'error', message: profileData.error })
if (localStorage.getItem('deviceTheme')) {
setDeviceTheme(JSON.parse(localStorage.getItem('deviceTheme')))
} else if (profileData.theme) {
setDeviceTheme(JSON.parse(JSON.stringify(profileData.theme)))
} else {
setDeviceTheme(settings.defaultTheme)
}
setProfile(profileData)
}
// Click event for menu close if clicked outside somewhere and other
const [clickEvent, setClickEvent] = useState(false)
const handleClick = event => {
setClickEvent(event)
}
// Or in some component user can change theme just like that
setDeviceTheme({color: red})
The main consideration (from a performance standpoint) for what to group together is less about which ones are used together and more about which ones change together. For things that are mostly set into context once (or at least very infrequently), you can probably keep them all together without any issue. But if there are some things mixed in that change much more frequently, it may be worth separating them out.
For instance, I would expect deviceTheme to be fairly static for a given user and probably used by a large number of components. I would guess that popup might be managing something about whether you currently have a popup window open, so it probably changes with every action related to opening/closing popups. If popup and deviceTheme are bundled in the same context, then every time popup changes it will cause all the components dependent on deviceTheme to also re-render. So I would probably have a separate PopupContext. windowSize and windowScroll would likely have similar issues. What exact approach to use gets deeper into opinion-land, but you could have an AppContext for the infrequently changing pieces and then more specific contexts for things that change more often.
The following CodeSandbox provides a demonstration of the interaction between useState and useContext with context divided a few different ways and some buttons to update the state that is held in context.
You can go to this URL to view the result in a full browser window. I encourage you to first get a handle for how the result works and then look at the code and experiment with it if there are other scenarios you want to understand.
This answer already does a good job at explaining how the context can be structured to be more efficient. But the final goal is to make context consumers be updated only when needed. It depends on specific case whether it's preferable to have single or multiple contexts.
At this point the problem is common for most global state React implementations, e.g. Redux. And a common solution is to make consumer components update only when needed with React.PureComponent, React.memo or shouldComponentUpdate hook:
const SomeComponent = memo(({ theme }) => <div>{theme}</div>);
...
<AllContext>
{({ deviceTheme }) => <SomeComponent theme={deviceTheme}/>
</AllContext>
SomeComponent will be re-rendered only on deviceTheme updates, even if the context or parent component is updated. This may or may not be desirable.
The answer by Ryan is fantastic and you should consider that while designing how to structure the context provider hierarchy.
I've come up with a solution which you can use to update multiple values in provider with having many useStates
Example :
const TestingContext = createContext()
const TestingComponent = () => {
const {data, setData} = useContext(TestingContext)
const {value1} = data
return (
<div>
{value1} is here
<button onClick={() => setData('value1', 'newline value')}>
Change value 1
</button>
</div>
)
}
const App = () => {
const values = {
value1: 'testing1',
value2: 'testing1',
value3: 'testing1',
value4: 'testing1',
value5: 'testing1',
}
const [data, setData] = useState(values)
const changeValues = (property, value) => {
setData({
...data,
[property]: value
})
}
return (
<TestingContext.Provider value={{data, setData: changeValues}}>
<TestingComponent/>
{/* more components here which want to have access to these values and want to change them*/}
</TestingContext.Provider>
)
}
You can still combine them! If you are concerned about performance, you can create the object earlier. I don't know if the values you use change, if they do not it is quite easy:
state = {
allContextValue: {
setProfile,
profileReload,
deviceTheme,
setDeviceTheme,
clickEvent,
usePopup,
popup,
windowSize
}
}
render() {
return <AllContext.Provider value={this.state.allContextValue}>...</AllContext>;
}
Whenever you then want to update any of the values you need to do I like this, though:
this.setState({
allContextValue: {
...this.state.allContextValue,
usePopup: true,
},
});
This will be both performant, and relatively easy as well :)
Splitting those up might speed up a little bit, but I would only do that as soon as you find it is actually slow, and only for parts of your context that would have a lot of consumers.
Still, if your value does not change a lot, there is really nothing to worry about.
Based on Koushik's answer I made my own typescipt version.
import React from "react"
type TestingContextType = {
value1?: string,
value2?: string,
value3?: string,
value4?: string,
value5?: string,
}
const contextDefaultValues = {
data: {
value1: 'testing1',
value2: 'testing1',
value3: 'testing1',
value4: 'testing1',
value5: 'testing1'
} as TestingContextType,
setData: (state: TestingContextType) => {}
};
const TestingContext = React.createContext(contextDefaultValues);
const TestingComponent = () => {
const {data, setData} = React.useContext(TestingContext);
const {value1} = data
return (
<div>
{value1} is here
<button onClick={() => setData({ value1 : 'newline value' })}>
Change value 1
</button>
</div>
)
}
const App = () => {
const [data, setData] = React.useState(contextDefaultValues.data)
const changeValues = (value : TestingContextType) => setData(data && value);
return (
<TestingContext.Provider value={{data, setData: changeValues}}>
<TestingComponent/>
{/* more components here which want to have access to these values and want to change them*/}
</TestingContext.Provider>
)
}
I understand basically what keys are for: we need to be able to identify changes in a list of children.
What I'm having trouble with is the information flow, and maintaining synchronicity between the states in my store and the states of subcomponents.
For example, say I have a list of <CustomTextInput> which is exactly what it sounds like. Some container with a text input inside of it. Many of them are stored in some <CustomTextInputList> element.
The number of CustomTextInputs in the list can change, they can be added and subtracted. I have a factory with an incrementing counter that issues new keys every time a CustomTextInput is inserted, no matter where it's placed.
I have a CustomTextInputModel type which populates my store. Whenever I change the value inside one of the inputs, it needs to call a callback which dispatches actions in my store. But then how do I know which one to modify, and how can I be sure that the whole list doesn't rerender from changing a single instance since the entire store's state is being recreated? Do I need to store a reference to some model ID in every CustomTextInput? Should this be the key?
Manythanks, I'm very new at this :)
But then how do I know which one to modify, and how can I be sure that the whole list doesn't re-render from changing a single instance since the entire store's state is being recreated?
If your component is tied to a list of objects in the redux store, then it will re-render the whole list since the parent would be getting new props. However there are a few ways to avoid the re-render.
Note that this is a pretty advanced post, but hopefully it helps. I find this page to be useful in explaining one way to make a react-redux application more performant in rendering large lists by having each item connected to the redux store and listening to itself via an id that is passed in via a prop.
Edit Heres another good article that illustrates the point: https://medium.com/devtravel/optimizing-react-redux-store-for-high-performance-updates-3ae6f7f1e4c1#.3ltrwmn3z
Inside of the Reducer:
function items(state = {}, action) {
switch (action.type) {
case 'MARK':
const item = state[action.id];
return {
...state,
[action.id]: {...item, marked: !item.marked}
};
default: return state;
}
}
function ids(state = [], action) {
return state;
}
In the list container:
<div>
{
ids.map(id => {
return <Item key={id} id={id} />;
})
}
</div>
// Returns an array of just ids, not whole objects
function mapStateToProps(state) {
return {id: state.ids};
}
Inside of the Item component:
// Uses id that is passed from parent, returns the item at the index equal to the id
function mapStateToProps(state, props) {
const { id } = props;
const { items } = state;
return {
item: items[id],
};
}
const markItem = (id) => ({type: 'MARK', id});
export default connect(
mapStateToProps,
{markItem}
)(Item);
Do I need to store a reference to some model ID in every CustomTextInput?
Yes, you will need some type of key/id to pass to the redux actions when you would like to make a modification to that specific item.
Should this be the key?
Most common way to reference something in the in the redux store would be by id.