React change state from another component without passing setState method - reactjs

I have a custom table component and it has two props: items and columns. I want to implement a sorting feature. Sorting is not a big deal. I am sorting the items inside the table component but when items are sorted, also the state that stores the items must be changed which is outside of my table component. I don't want to pass setState method cause of my component is generic. It would be very useless if I pass setState method everywhere.
How do popular libraries solve this problem without need of pass a state-altering method? Do they copy the state to an internal state and then modify it or something? I hope I could explain my problem.

It might help to think in terms of controlled vs uncontrolled components. You may be familiar with this from core elements like <input>s, where you can either pass in a defaultValue prop, and let the input handle everything ("uncontrolled"), or you can pass in value and onChange and handle things yourself ("controlled"). You can design your table component as either a controlled component or uncontrolled component too.
Doing it as an uncontrolled component, you might pass in a prop that sets the initial sorting, but afterwards everything is handled by the table. The parent won't get notified and won't update its state:
const Parent = () => {
const [items, setItems] = useState(/* some array */);
return <MyTable items={items} defaultSort="asc" />
}
const MyTable = ({ items, defaultSort }) => {
const [sort, setSort] = useState(defaultSort ?? 'asc');
const sortedItems = useMemo(() => {
if (sort === 'asc') {
return [...items].sort(/* insert sort function here */)
} else {
return [...items].sort(/* insert sort function here */)
}
}, [items, sort]);
return (
<>
<button onClick={() => setSort(sort === 'asc' ? 'dsc' : 'asc')}>
Change Sort
</button>
{sortedItems.map(() => /* etc */)}
</>
)
}
If instead you do a controlled component, then the parent is in charge of the state, and the child just notifies the parent of relevant changes
const Parent = () => {
const [items, setItems] = useState(/* some array */);
const [sort, setSort] = useState('asc');
const sortedItems = useMemo(() => {
if (sort === 'asc') {
return [...items].sort(/* insert sort function here */)
} else {
return [...items].sort(/* insert sort function here */)
}
}, [items, sort]);
return <MyTable items={sortedItems} onSortToggled={() => setSort(sort === 'asc' ? 'dsc' : 'asc')} />
}
const MyTable = ({ items, onSortToggled}) => {
return (
<>
<button onClick={onSortToggled}>
Change Sort
</button>
{items.map(() => /* etc */)}
</>
)
}
If you add in some extra code to check for undefineds, it's possible to make your table support both controlled and uncontrolled modes, based on which set of props it is passed. But it should just be one or the other; you shouldn't try to have both components be managing state simultaneously, as this just adds opportunities for the states to get out of sync and bugs to be introduced.
the state that stores the items must be changed which is outside of my table component
If this is one of your requirements, then you're basically doing the controlled component version, and thus you must accept a function from the parent component which describes how to do so. The parent component is the only one who knows what state they have and how to update it.

Related

Basis of two props wanted to update single state value without useEffect

What is the best way to update single state value, if specific two props changes, without useEffect, since performance point of view we should not be setting state inside useEffect.
For Example:
const NestedList = ({childItems, parentItems, selected, ...rest}) => {
const [currentItems, setCurrentItems] = useState({}); //assume this could be hash object
// Wrong X
useEffect(() => setCurrentItems(parentItems), [parentItems])
// Wrong X
useEffect(() => setCurrentItems(childItems), [childItems])
return ...
React component updates on both State change and Prop change. So you don't have to do anything additonal
Try below in single use effect. You can also compare previous props with new props.
useEffect(() => {
setCurrentItems(whateverthevalueis)
}, [prop1,prop2]);
What i try to figure out is the why in this case.
Why do you need a state if you always want to change it?
Why do the two props replace each other in the state?
There is nothing wrong with setting the state in a useEffect as alot of people already have said. However useEffect should be used for side effects.
There is no golden solution for all cases.
Use a state if you set the intial state with props and then the component update the state itself.
const processItems = (childItems, parentItems, ) => {
// Do what you want to create one state
return childItems
}
const NestedList = ({childItems, parentItems, selected, ...rest}) => {
// use props to set the inital state
const [currentItems, setCurrentItems] = useState(processItems(childItems, parentItems))
// We can later mutate the state, however if the props change we do not care
If the parent always pass the state as a prop, you do not need a state since the state is handled higher up.
const NestedList = ({childItems, parentItems, selected, ...rest}) => {
// No need for a state here, we can just use the props directly and
// the parent will pass the updated props when the state changes
If the parent pass several props and you need to calculate stuff based on the props, use memo hook.
const NestedList = ({childItems, parentItems, selected, ...rest}) => {
const currentItems = useMemo(() => {
// Add the condition of what items to show here
if(parentItems.length > 0) return parentItems
return childItems
}, [childItems,parentItems])
return ...

useEffect is not triggered by a change in ref.current from a child component

I have created a popup that the user can use to add stuff to the application, every field is a separate component, because I need to reuse them in several places in different configruations.
I have tried to create an innerRef that when changed (i.e. new value is typed), the useEffect of the component should be triggered to show or hide the Done button if all values are valid.
I know that all values are valid or not from the valid prop that I assign to .current
export default function AddStock() {
const selectTypeOfQuantityRef = useRef({});
const [allValid, setAllValid] = useState(false);
useEffect(() => {
const allValues = [selectTypeOfQuantityRef.current.valid];
allValues.every((value) => value) ? setAllValid(true) : setAllValid(false);
console.log(allValues.every((value) => value)); // does not get triggered
}, [selectTypeOfQuantityRef.current]);
return (
<>
<AddPopup>
<SelectTypeOfQuantity innerRef={selectTypeOfQuantityRef} />
{allValid && <DoneButton/>}
<CancelButton/>
</AddPopup>
</>
);
}
And this is the select itself (custom of course), that sets innerRef, whenever its state changes.
Everything here works, the state of this small component itself is managed correctly, but it just does not get triggered the state update of the parent component
export default function SelectTypeOfQuantity({ defaultValue = null, innerRef }) {
const [selectTypeOfQuantity, setSelectTypeOfQuantity] = useState(defaultValue);
const [valid, setValid] = useState(false);
const [errMessage, setErrMessage] = useState("Избери стойност!");
useEffect(() => {
innerRef.current.value = selectTypeOfQuantity;
handleValidation(selectTypeOfQuantity);
}, [selectTypeOfQuantity]);
const handleValidation = (value) => {
const result = validateAutocomplete(value);
if (result.valid) {
setValid(true);
setErrMessage(null);
innerRef.current.valid = result.valid;
} else {
setValid(false);
setErrMessage(result.errMessage);
}
};
const selectTypeOfQuantityOnChange = (e, val) => {
setSelectTypeOfQuantity(val ? val.value : null);
};
return (
<Select onChange={selectTypeOfQuantityOnChange}/>
);
}
useRef does not trigger rerenders, thus useEffect will not be called
Use useRef when you need information that is available regardless of component lifecycle and whose changes should NOT trigger rerenders. Use useState for information whose changes should trigger rerenders.
Solution
As React's Philosophy states, all the data must reside within React, that's why even input components are supplied with a value and onChange event. React can't track data changes that happens outside it. As I understand from your question, the changes are happending within the React App, So instead of tracking the data through the innerRef, track them within React using React's own methods.

Updating one State with two fields?

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

React useState vs raw variable

For example i have a some data which need to be rendered. Items will be always the same but they are coming from props.
const items = props.data.values.map(value => ({
id: value.id
name: value.name,
rating: Number(value.rating)
}));
return (
{items.map(item => (
<div key={item.id}....
)}
);
May i use useState for items variable like that:
const [items] = useState(data.values.map(value => ({
id: value.id
name: value.name,
rating: Number(value.rating)
})));
Does it's help me to get rid of redundant "mapping" during next rerender or not?
No, it doesn't help.
I think it's better to completely get rid of the first map and do whatever you want in the second one. but if you think that is necessary for your app, you could use useMemo hook.
This hook provides you a memoized value that re-calculates only if some parameters change.
For example:
const items = useMemo(() => {
return data.values.map(...)
}, [data])
This example calculates items only if the value of data changes. otherwise, it returns the memoized version and doesn't re-calculate anything.
But what about useState? it used whenever we have some variable that whenever it changes, we want to re-render our component and show some new contents. for example, we have a Counter that whenever its value changes, we want to re-render component and show the new Value. so we use something like this:
const Counter = (props) => {
const [value, setValue] = useState(0)
return (
<div>
<p>{value}</p>
<button onClick={() => setValue(value + 1)}>Increment</button>
<button onClick={() => setValue(value - 1)}>Decrement</button>
</div>
)
}
So whenever we call setValue, the component re-renders and the new value will be shown to the user.
I think what you're looking for is Pure Components. They use shouldComponentUpdate to determine if the component needs to re-render. In your case if the props are the same, the component won't re-render if you use a Pure Component.

How to use React Hooks Context with multiple values for Providers

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

Resources