When there is a new update in my parent component, which occur within the function updateItemValue() , the items state will only get updated in parent. The child component won't get updated in an instance. It only get updated when the updateItemValue() get triggered twice.
It's something like:
1st round, parent update the items, child didn't receive the update.
2nd round, parent update the items, child only receive the update from 1st round
Parent Component
const SelectItem = () => {
const { items } = useSelector(state => state.itemsReducer);
const [ itemUpdate, setItemUpdate ] = useState(false);
const [ group_id, setGroupId ] = useState(false);
useEffect(() => {
updateItemValue()
}, [itemUpdate])
function updateItemValue(){
// original items value
/*
[
{"_id":1, "name":"Item 1", "description":"Item 1 description"},
{"_id":2, "name":"Item 2", "description":"Item 2 description"},
{"_id":3, "name":"Item 3", "description":"Item 3 description"}
]
*/
// step 3 : now this function get called because the group_id is updated
items.forEach((item, i) => {
if (group_id == item.group_id) {
// step 4 : update the value of items state, add extra data, once this updated, I expect the new items value to be passed to child component
items[i].extra = true;
}
});
}
// step 2 : update the group_id and set the itemUpdate to true to trigger useEffect
function selectGroup(group_id){
setItemUpdate(true)
setGroupId(group_id)
}
return(
<div>
{/* step 1: select option */}
<button onClick={() => selectGroup(1)}>Option 1</button>
<button onClick={() => selectGroup(2)}>Option 2</button>
{
items.map((item) => (
<ItemCard
key={item._id}
id={item._id}
name={item.name}
description={item.description}
onClick={(value) => updateSelectedItem(value)}
/>
))
}
</div>
)
}
Child Component
export default function ItemCard({id, name, description, onClick, extra}){
console.log(extra) // the value is undefined when selectGroup() 1st triggered from parent
return (
<div onClick={() => onClick(id)}>
<div>
<p>{name}</p>
<p>{description}</p>
{ extra ? '<p>Extra item is required</p>' : null }
</div>
</div>
)
}
p/s : the state items came from redux state. So the state update at updateItemValue() is to add some extra value which I want to use at child component.
Issue
You are mutating an object reference
function updateItemValue(){
items.forEach((item, i) => {
if (group_id == item.group_id) {
items[i].extra = true; // <-- state object mutation!
}
});
}
Solution
Looks like you are using Redux since you are using the useSelector react hook.
const { items } = useSelector(state => state.itemsReducer);
I'll assume you have access to a useDispatch hook and action to dispatch to update your state as well.
You can likely simplify your component code a bit, and move the update logic to the reducer so stat is correctly updated.
const SelectItem = () => {
const { items } = useSelector((state) => state.itemsReducer);
const dispatch = useDispatch();
// step 2: dispatch action to update state via the group_id
function selectGroup(groupId) {
dispatch({ type: "UPDATE_GROUP", groupId });
}
return (
<div>
{/* step 1: select option */}
<button onClick={() => selectGroup(1)}>Option 1</button>
<button onClick={() => selectGroup(2)}>Option 2</button>
{items.map((item) => (
<ItemCard
key={item._id}
id={item._id}
name={item.name}
description={item.description}
onClick={(value) => updateSelectedItem(value)}
/>
))}
</div>
);
};
I'm going to just guess at the reducer function now, it may be similar to the following.
const initialState = {
items: [],
}
const itemsReducer = (state = initialState, action) => {
switch (action.type) {
...
// step 3: update items array by matching item group id
case "UPDATE_GROUP":
return {
...state,
items: state.items.map((item) =>
item.group_id === action.groupId
? {
...item,
extra: true
}
: item
)
};
...
default:
return state;
}
};
Additional Suggestion
You can also simplify the child component ItemCard a bit. Direct attach the click handler and simplify the conditional rendering. Also, I don't know if it was intentional (I think not), but you may not want the string literal "<p>Extra item is required</p>" rendering.
function ItemCard({ name, description, onClick, extra }) {
return (
<div onClick={onClick}>
<div>
<p>{name}</p>
<p>{description}</p>
{extra && <p>Extra item is required</p>}
</div>
</div>
);
}
And update the mapping in the parent. Don't pass an id prop, but pass the item._id to the updateSelectedItem callback.
{items.map((item) => (
<ItemCard
key={item._id}
name={item.name}
description={item.description}
onClick={() => updateSelectedItem(item._id)}
/>
))}
Related
React Hook "useState" cannot be called inside a callback. React Hooks must be called in a React function component or a custom React Hook function
I understand why I am getting that message, but at the same time I think what I want should be achievable. Here's the code:
import React, { useState } from "react";
import "./styles.css";
export default function App() {
const children = useState([]);
return (
<div>
{children.map((child, i) => (<Child child={child} />))}
<button
onClick={() => { children[1]([...children, useState(0)]); }}
>
Add
</button>
</div>
);
}
function Child(props) {
const [state, setState] = props.child;
return (
<div>
<input
type="range"
min="1"
max="255"
value={state}
onChange={(e) => setState(e.target.value)}
></input>
</div>
);
}
I want each <Child> to be in complete control of its state without having to declare a function of type updateChild(i : Index, data) then finding the right children in the list, etc. As this doesn't scale well with deep nested view hierarchies.
Is there a way to both:
Keep the state in the parent (single source of truth)
Allow children to mutate their own state
In a nutshell, I want to achieve this but with more than one child component.
To illustrate the problem with your current approach and why React won't let you do that, here's an ersatz implementation of useState that has approximately the same behaviour as the real version (I've left triggering a re-render as an exercise to the user and it doesn't support functional updates, but the important thing here was showing the underlying state of the Parent component).
// Approximation of useState
let parentStateIndex;
const parentState = [];
const useState = (defaultValue) => {
const i = parentStateIndex;
if (parentState.length === i) {
parentState.push(defaultValue);
}
parentStateIndex += 1;
return [parentState[i], (value) => parentState[i] = value];
}
const Parent = () => {
parentStateIndex = 0; // hack required to reset on each "render"
const [state, setState] = useState([]);
return (
<>
{state.map((child) => (
<Child child={child} />
)}
<button
onClick={() => setState([...state, useState(0)])}
>
Add
</button>
</>
);
};
const Child = ({ child }) => {
const [state, setState] = child;
return (
<input
type="range"
min="0"
max="255"
value={state}
onChange={({ target }) => setState([target.value, setState])}
/>
);
};
(Note a bit of knowledge about the parent state has already crept into the child here - if it only setState(target.value) then the [value, setter] pair would be replaced by just value and other things would start exploding. But I think this way around gives a better illustration of what happens further down.)
The first time the parent is rendered, the new array passed to useState is added to the state:
parentState = [
[
[],
(value) => parentState[0] = value
],
];
All good so far. Now imagine the Add button is clicked. useState is called again, and the state is updated to add a second item and add that new item to the first item:
parentState = [
[
[
[0, (value) => parentState[1] = value]
],
(value) => parentState[0] = value,
],
[
0,
(value) => parentState[1] = value,
],
];
This also seems to have worked, but now what happens when the child value updates to e.g. 1?
parentState = [
[
[
[0, /* this gets called: */ (value) => parentState[1] = value],
],
(value) => parentState[0] = value,
],
[
/* but this gets changed: */ 1,
(value) => parentState[1] = value,
],
];
The second part of the state is updated, which changes its type completely, but the first one still holds the old value.
When the parent re-renders, useState is is only called once, so the second item in the parent state is irrelevant; the child gets the old value parentState[0][0], which is still [0, () => ...].
When the Add button gets clicked again, because that's only the second time useState gets called on this render, now we get the new value that was intended for the first child, as the second child:
parentState = [
[
[
[0, (value) => parentState[1] = value],
[1, (value) => parentState[1] = value],
],
(value) => parentState[0] = value,
],
[
1,
(value) => parentState[1] = value,
],
];
And, as you can see, changes to either the first or second child both target the same value; they would not actually appear anywhere until the Add button was clicked again and they'd suddenly be the value of the third child.
For more on how hooks work and why the call order is so important, see e.g. "Why Do React Hooks Rely on Call Order?" by Dan Abramov.
So what would work? For the child to be able to correctly update its parent's state, it needs at least the setter and its own index:
const Parent = () => {
const [state, setState] = useState([]);
return (
<>
{state.map((child, index) => (
<Child child={child} index={index} setState={setState} />
)}
<button
onClick={() => setState([...state, 0])}
>
Add
</button>
</>
);
};
const Child = ({ child, index, setState }) => {
return (
<input
type="range"
min="0"
max="255"
value={child}
onChange={({ target }) => setState((oldState) => {
return oldState.map((oldValue, i) => i === index
? target.value
: oldValue);
})}
/>
);
};
But that means the parent no longer controls its own state, and the child has to know all about its parent's state - those two components are very closely coupled, so the boundary between them is clearly in the wrong place (and maybe shouldn't exist at all).
So what's the correct solution? It's the thing you didn't want to do, having the child "phone home" with an updated value and letting the parent update its state accordingly. This keeps the child decoupled from the details of the parent and its implementation correspondingly simple:
const Parent = () => {
const [state, setState] = useState([]);
const onChange = (newValue, index) => setState((oldState) => {
return oldState.map((oldValue, i) => i === index
? newValue
: oldValue);
};
return (
<>
{state.map((child, index) => (
<Child child={child} onChange={(value) => onChange(value, index)} />
)}
<button
onClick={() => setState([...state, 0])}
>
Add
</button>
</>
);
};
const Child = ({ child, onChange }) => {
return (
<input
type="range"
min="0"
max="255"
value={child}
onChange={({ target: { value } }) => onChange(value)}
/>
);
};
In my container component I have a state that gets initialized with an object that I use as data.
I clone the state array to prevent the initial state from being mutated but it still gets mutated, which I don't want to happen since I will need to compare the current state with the initial state later on.
The who system is kept inside the CubeOfTruthSystem component
function CubeOfTruthSystem() {
const [cubeIndex, setCubeIndex] = useState(0);
const [faceIndex, setFaceIndex] = useState(0);
return (
<React.Fragment>
<CubeSelector handleClick={(index) => setCubeIndex(index)} />
<CubeContainer cubeIndex={cubeIndex} faceIndex={faceIndex} />
<FaceSelector handleClick={(index) => setFaceIndex(index)} />
<button id="reset-face" onClick={() => console.log(CubeOfTruth)}>
Reset
</button>
</React.Fragment>
);
}
The parent component for the state looks like this:
function CubeContainer({ cubeIndex, faceIndex }) {
const [cube, setCube] = useState(CubeOfTruthData);
const handleCellClick = (id, row) => {
const cubeClone = [...cube];
const item = cubeClone[cubeIndex].faces[faceIndex].data[0].find(
(item) => item.id === id
);
item.state = "active";
cubeClone[cubeIndex].faces[faceIndex].data = activateAdjacentCells(
id,
row,
cubeClone[cubeIndex].faces[faceIndex].data,
item
);
setCube(cubeClone);
};
return (
<div id="cube-container">
{cube[cubeIndex].faces[faceIndex].data.map((row) => {
return row.map((item) => {
return (
<CubeItem item={item} handleClick={handleCellClick} key={item.id} />
);
});
})}
</div>
);
}
And this is the child component
function CubeItem({ item, handleClick }) {
const handleBgClass = (cellData) => {
if (cellData.state === "inactive") {
return cellData.bg + "-inactive";
} else if (cellData.state === "semi-active") {
return cellData.bg + "-semi-active";
} else {
return cellData.bg;
}
};
return (
<button
className={`cell-item ${handleBgClass(item)}`}
disabled={item.state === "inactive" ? true : false}
onClick={() => handleClick(item.id, item.row)}
/>
);
}
In the CubeOfTruth component, I'm trying to get the initial state (which is the CubeOfTruth array), but after changing the state, cube, cubeClone and CubeOfTruth all have the same values.
How can I make sure CubeOfTruth never gets mutated?
You're trying to clone cube array but you're making a shallow copy of it.
If you want to prevent mutation of nested properties you should make a deep copy instead.
Replace this:
const cubeClone = [...cube];
With this:
const cubeClone = JSON.parse(JSON.stringify(cube));
Or use some library like lodash
const cubeClone = _.cloneDeep(cube)
Hello i am currently facing this situation.
here is the simplified version of the code.
const Parent = ({prop}) => {
const [listOfBool, setListOfBool] = useState([true, false])
const handleCallback = (e, ev) => {
var cloneListOfBool = [...listOfBool]
cloneListOfBool[e] = ev
setListOfBool(cloneListOfBool)
}
return (
<div>
<p>{prop}</p>
<div>{listOfBool.map((bool, idx) => <Child key={idx} prop1={idx} activeProp={bool} parentCallback={handleCallback} />)}</div>
</div>
)
}
this is the child component
const Child = ({prop1, activeProp, parentCallback}) => {
const [active, setActive] = useState(activeProp)
const setThis = (e) => {
if (active === true){
parentCallback(e, false)
setActive(false)
} else {
parentCallback(e, true)
setActive(true)
}
}
return (
<>
<p className={`${active === true ? 'selected' : ''}`} onClick={() => setThis(prop1)}>{prop1}</p>
</>
)
}
prop1 is a number, i use that number to acces the array and change its value.
in the parent i set the list of boolean values, i map through them and i create the childrens. Now when the props of the parent changes, i would like to re render every child . Everything works as i want except for this part. Later on i will need to make a request to get the list of bools. Can you tell me waht is wrong, i have tried a couple of different solutions with no succes. Thank You
You can use key to force a react component to rerender.
So in this case if prop is the parent prop that you want to listen to, you can do something similar to:
<div>
{listOfBool.map((bool, idx) => (
<Child
key={`idx_${prop}`}
prop1={idx}
active={bool}
parentCallback={handleCallback}
/>
))}
</div>
May I ask why you want the component to rerender on a prop it does not use? As much as possible, you should just pass that property to the child, as a setup like this implies some sort of side effect that might be hard to spot.
Update (see comments)
const Parent = ({prop}) => {
const [listOfBool, setListOfBool] = useState([true, false])
const setActiveAtIndex = (idx, active) => {
setListOfBool((list) => {
const newList = [...list]
newList[idx] = active
return newList
})
}
return (
<div>
<p>{prop}</p>
<div>
{listOfBool.map((bool, idx) => (
<Child
key={idx}
prop1={idx}
activeProp={bool}
setActive={active => setActiveAtIndex(idx, active)}
/>
))}
</div>
</div>
)
}
const Child = ({prop1, active, setActive, parentCallback}) => {
return (
<>
<p className={`${active ? 'selected' : ''}`} onClick={() => setActive(!active)}>{prop1}</p>
</>
)
}
There are few problems with your code. I think the children are re-rendered, but if you expect the className name to change that's not gonna happed.
prop1 is a number, but you are using a === equality when setting active. This means that it will always be false.
The second problem is that you're deconstructing props and get a var named active and then declare another one with the same name, shadowing the prop one.
You should have only one source of truth and that's the parent in this case. There is no need for a child state.
const Child = ({prop1, active, parentCallback}) => {
return (
<>
<p className={`${active === true ? 'selected' : ''}`} onClick={() => parentCallback(prop1, !active)}>{prop1}</p>
</>
)
}
And also, prop1 is a bad name.
I am trying to remove an input field with filter function but it's not working.
In the following code add operation works fine but remove operation is not working properly ,it is not removing the corresponding element.Another problem the values on the inputs fields not present when the component re-renders.so experts guide me how i can achieve removing the corresponding row when the remove button is clicked and the input values should not be reset when the component re-renders
So when I refresh the page and click to remove an input it will clear all other input data. How can I fix this problem ?
Update adding full component in question:
const Agreement = (props) => {
const { agreement, editable, teamData, teamId, fetchTeamData } = props;
const [editing, setEditing] = useState(false);
const [title, setTitle] = useState("");
const [showErrors, setShowErrors] = useState(false);
const [errorsArr, setErrorsArr] = useState();
const initialFormState = {
rule_0: teamData.rules.rule_0,
rule_1: teamData.rules.rule_1,
rule_2: teamData.rules.rule_2,
rule_3: teamData.rules.rule_3,
creator: teamData.User.public_user_id,
};
const [updateTeamData, setUpdateTeamData] = useState(initialFormState);
const [inputs, setInputs] = useState(
teamData.rules.map((el) => ({
...el,
guid: uuidV4(),
}))
);
const handleChange = (event) => {
const { name, value } = event.target;
// Update state
setUpdateTeamData((prevState) => ({
...prevState,
[name]: value,
}));
};
// Add more input
const addInputs = () => {
setInputs([...inputs, { name: `rule_${inputs.length + 1}` }]);
};
// handle click event of the Remove button
const removeInputs = (index) => {
const newList = inputs.filter((item, i) => index !== i); // <-- compare for matching index
setInputs(newList);
};
const clearInput = (dataName) => {
setUpdateTeamData((prevState) => {
delete prevState[dataName];
return {
...prevState,
};
});
};
const handleSubmit = async (event) => {
event.preventDefault();
setEditing(false);
// Send update request
const res = await axios.put(`/api/v1/teams/team/${teamId}`, updateTeamData);
// If no validation errors were found
// Validation errors don't throw errors, it returns an array to display.
if (res.data.validationErrors === undefined) {
// Clear any errors
setErrorsArr([]);
// Hide the errors component
setShowErrors(false);
// Call update profiles on parent
fetchTeamData();
} else {
// Set errors
setErrorsArr(res.data.validationErrors.errors);
// Show the errors component
setShowErrors(true);
}
};
const handleCancel = () => {
setEditing(false);
};
useEffect(() => {
if (agreement === "default") {
setTitle(defaultTitle);
// setInputs(teamData.rules);
} else {
setTitle(agreement.title ?? "");
}
}, [agreement, teamData]);
// console.log("teamData.rules", teamData);
console.log("inputs", inputs);
return (
<div className="team-agreement-container">
{!editing && (
<>
<h4 className="team-agreement-rules-title">{title}</h4>
{editable && (
<div className="team-agreement-rules">
<EditOutlined
className="team-agreement-rules-edit-icon"
onClick={() => setEditing(true)}
/>
</div>
)}
{teamData.rules.map((rule, index) => (
<div className="team-agreement-rule-item" key={`rule-${index}`}>
{rule ? (
<div>
<h4 className="team-agreement-rule-item-title">
{`Rule #${index + 1}`}
</h4>
<p className="team-agreement-rule-item-description">
- {rule}
</p>
</div>
) : (
""
)}
</div>
))}
</>
)}
{/* Edit rules form */}
{editing && (
<div className="team-agreement-form">
{showErrors && <ModalErrorHandler errorsArr={errorsArr} />}
<h1>Rules</h1>
{inputs.map((data, idx) => {
return (
<div className="agreement-form-grid" key={data.guid}>
<button
type="button"
className="agreement-remove-button"
onClick={() => {
removeInputs(idx);
clearInput(`rule_${idx}`);
}}
>
<Remove />
</button>
<input
name={`rule_${idx}`}
onChange={handleChange}
value={teamData.rules[idx]}
/>
</div>
);
})}
{inputs.length < 4 && (
<div className="team-agreement-add-rule">
<button type="submit" onClick={addInputs}>
<Add />
</button>
</div>
)}
<div className="div-button">
<button className="save-button" onClick={handleSubmit}>
Save
</button>
<button className="cancel-button" onClick={handleCancel}>
Cancel
</button>
</div>
</div>
)}
</div>
);
};
export default Agreement;
When i do console.log(inputs) this is the data that I got:
0: 0: "t" 1: "e" 2: "s" guid: "e18595a5-e30b-4b71-8fc2-0ad9c0e140b2"
proto: Object 1: 0: "d" 1: "a" 2: "s" 3: "d" 4: "a" 5: "s" guid: "537ca359-511b-4bc6-9583-553ea6ebf544" ...
Issue
The issue here is that you are using the array index as the React key. When you mutate the underlying data and reorder or add/remove elements in the middle of the array then the elements shift around but the React key previously used doesn't move with the elements.
When you remove an element then all posterior elements shift forward and the index, as key, remains the same so React bails on rerendering the elements. The array will be one element shorter in length and so you'll see the last item removed instead of the one you actually removed.
Solution
Use a React key that is intrinsic to the elements being mapped, unique properties like guids, ids, name, etc... any property of the element that guarantees sufficient uniqueness among the dataset (i.e. the siblings).
const [inputs, setInputs] = useState(teamData.rules);
const removeInputs = (index) => {
// compare for matching index
setInputs(inputs => inputs.filter((item, i) => index !== i));
};
{inputs.map((data, idx) => {
return (
<div className="agreement-form-grid" key={data.id}> // <-- use a unique property
<button
type="button"
className="agreement-remove-button"
onClick={() => {
removeInputs(idx);
clearInput(`rule_${idx}`);
}}
>
<Remove />
</button>
<input
name={`rule_${idx}`}
onChange={handleChange}
value={teamData.rules[idx]}
/>
</div>
);
})}
If your teamData.rules initial state value doesn't have any unique properties to use then you can map this to a new array and add a sufficient id property.
const [inputs, setInputs] = useState(teamData.rules.map(el => ({
...el,
guid: generateId()***,
})));
*** this is a function you need to define yourself, or import from a module like uuid***
import { v4 as uuidV4 } from 'uuid';
...
const [inputs, setInputs] = useState(teamData.rules.map(el => ({
...el,
guid: uuidV4(),
})));
// Add more input
const addInputs = () => {
setInputs(inputs => [
...inputs,
{
name: `rule_${inputs.length + 1}`,
guid: uuidV4();
},
]);
};
Then when mapping use the guid property.
<div className="agreement-form-grid" key={data.guid}>
The issue is because you are trying to compare index with array item in filter method. You should use the second argument in filter which denotes the array index of the current iterating item
const removeInputs = (index) => {
const newList = inputs.filter((item,i) => index !== i);
setInputs(newList);
};
That's your solution, you are trying with item but you are comparing it with index that's wrong. You should do it like this,
const newList = inputs.filter((item, key) => index !== key);
Lets say I have a parent component and child component. The parent component is composed of several child components. The parent component holds and manages a very complex and deep data object. Each child component provides the UI to manage various child objects and properties of the main data object. Whenever the child component changes a property value in the data object hierarchy, that change needs to bubble up to the main data object.
Here is how I might do it in a child component class by passing in a callback object...
<div>
<button onClick={e => this.setState({propA: e.target.value}, () => props.onChangePropA(this.state.propA)}>Prop A</button>
<button onClick={e => this.setState({propB: e.target.value}, () => props.onChangePropB(this.state.propB)}>Prop B</button>
</div>
Versus how I think I need to do it using hooks. The main problem I'm seeing is that there is no callback option for after the state change has completed. So I have to detect it in the useEffect and figure out which property just changed...
let prevPropA = props.propA;
let prevPropB = props.propB;
const [propA, setPropA] = useState(props.propA);
const [propB, setPropB] = useState(props.propB);
useEffect(() => {
if (prevPropA != propA) props.onChangePropA(propA);
if (prevPropB != propB) props.onChangePropB(propB);
});
<div>
<button onClick={e => {prevPropA = propA; setPropA(e.target.value)}}>Prop A</button>
<button onClick={e => {prevPropB = propB; setPropB(e.target.value)}}>Prop B</button>
</div>
I see this method getting extremely cumbersome and messy. Is there a more robust/proper way to accomplish this?
Thanks
=============================================================
Below is updated sample code based on Shubham's answer and
Ryan's feedback. Shubham answered the question as asked, but
Ryan is suggesting I give a more thorough example to ensure
I'm giving the right info for the right answer.
Here is sample code that more closely follows my real world
situation... although still a simplified example.
The parent component manages comments from users. Imagine
they can create new comments and select a date or a date-range.
They can also update existing comments. I have put the date
and date-range selector in its own component.
Therefore the parent comment manager component needs the ability
to create/load comments and pass the associated date(s) down to the
date-selector component. The user can then change the date(s)
and those values need to be propagated back up to the parent comment
manager to later be sent to the server and saved.
So you see, there is a bidirectional flow of property values (dates, etc)
that can be changed at any time from either end.
NOTE: This new example is updated using a method similar to what
Shubham suggested based on my original question.
=============================================================
const DateTimeRangeSelector = (props) =>
{
const [contextDateStart, setContextDateStart] = useState(props.contextDateStart);
const [contextDateEnd, setContextDateEnd] = useState(props.contextDateEnd);
const [contextDateOnly, setContextDateOnly] = useState(props.contextDateOnly);
const [contextDateHasRange, setContextDateHasRange] = useState(props.contextDateHasRange);
useEffect(() => { setContextDateStart(props.contextDateStart); }, [ props.contextDateStart ]);
useEffect(() => { if (contextDateStart !== undefined) props.onChangeContextDateStart(contextDateStart); }, [ contextDateStart ]);
useEffect(() => { setContextDateEnd(props.contextDateEnd); }, [ props.contextDateEnd ]);
useEffect(() => { if (contextDateEnd !== undefined) props.onChangeContextDateEnd(contextDateEnd); }, [ contextDateEnd ]);
useEffect(() => { setContextDateOnly(props.contextDateOnly); }, [ props.contextDateOnly ]);
useEffect(() => { if (contextDateOnly !== undefined) props.onChangeContextDateOnly(contextDateOnly); }, [ contextDateOnly ]);
useEffect(() => { setContextDateHasRange(props.contextDateHasRange); }, [ props.contextDateHasRange ]);
useEffect(() => { if (contextDateHasRange !== undefined) props.onChangeContextDateHasRange(contextDateHasRange); }, [ contextDateHasRange ]);
return <div>
<ToggleButtonGroup
exclusive={false}
value={(contextDateHasRange === true) ? ['range'] : []}
selected={true}
onChange={(event, value) => setContextDateHasRange(value.some(item => item === 'range'))}
>
<ToggleButton value='range' title='Specify a date range' >
<FontAwesomeIcon icon='arrows-alt-h' size='lg' />
</ToggleButton>
</ToggleButtonGroup>
{
(contextDateHasRange === true)
?
<DateTimeRangePicker
range={[contextDateStart, contextDateEnd]}
onChangeRange={val => { setContextDateStart(val[0]); setContextDateEnd(val[1]); }}
onChangeShowTime={ val => setContextDateOnly(! val) }
/>
:
<DateTimePicker
selectedDate={contextDateStart}
onChange={val => setContextDateStart(val)}
showTime={! contextDateOnly}
/>
}
</div>
}
const CommentEntry = (props) =>
{
const [activeComment, setActiveComment] = useState(null);
const createComment = () =>
{
return {uid: uuidv4(), content: '', contextDateHasRange: false, contextDateOnly: false, contextDateStart: null, contextDateEnd: null};
}
const editComment = () =>
{
return loadCommentFromSomewhere();
}
const newComment = () =>
{
setActiveComment(createComment());
}
const clearComment = () =>
{
setActiveComment(null);
}
return (
<div>
<Button onClick={() => newComment()} variant="contained">
New Comment
</Button>
<Button onClick={() => editComment()} variant="contained">
Edit Comment
</Button>
{
activeComment !== null &&
<div>
<TextField
value={(activeComment) ? activeComment.content: ''}
label="Enter comment..."
onChange={(event) => { setActiveComment({...activeComment, content: event.currentTarget.value, }) }}
/>
<DateTimeRangeSelector
onChange={(val) => setActiveComment(val)}
contextDateStart={activeComment.contextDateStart}
onChangeContextDateStart={val => activeComment.contextDateStart = val}
contextDateEnd={activeComment.contextDateEnd}
onChangeContextDateEnd={val => activeComment.contextDateEnd = val}
contextDateOnly={activeComment.contextDateOnly}
onChangeContextDateOnly={val => activeComment.contextDateOnly = val}
contextDateHasRange={activeComment.contextDateHasRange}
onChangeContextDateHasRange={val => activeComment.contextDateHasRange = val}
/>
<Button onClick={() => clearComment()} variant="contained">
Cancel
</Button>
<Button color='primary' onClick={() => httpPostJson('my-url', activeComment, () => console.log('saved'))} variant="contained" >
<SaveIcon/> Save
</Button>
</div>
}
</div>
);
}
useEffect takes a second argument which denotes when to execute the effect. You can pass in the state value to it so that it executes when state updates. Also you can have multiple useEffect hooks in your code
const [propA, setPropA] = useState(props.propA);
const [propB, setPropB] = useState(props.propB);
useEffect(() => {
props.onChangePropA(propA);
}, [propA]);
useEffect(() => {
props.onChangePropB(propB);
}, [propB]);
<div>
<button onClick={e => {setPropA(e.target.value)}}>Prop A</button>
<button onClick={e => {setPropB(e.target.value)}}>Prop B</button>
</div>