I fear that I've made a complete mess of my React structure.
I have a parent component/view that contains an object of attributes that are the key element of the content on the page. The ideal is to have a component that allows the user to update these attributes, and when they do so the content on the page updates. To avoid having the content update on every single attribute update/click, I want to implement a 'revert'/'save' button on the attribute update screen.
The problem I'm having is that I'm passing the state update function from the main parent to the child components, and the save is updating the parent and then the child switches out of 'edit mode' and displays the correct value, but when I click revert it doesn't update the parent (good) but it still maintains the local state of the child rather than rerendering to the 'true' parent state so the child implies it has been updated when it actually hasn't.
I'm a hobbyist React developer so I'm hoping that there is just something wrong with my setup that is easily rectifiable.
export const MainViewParent = () => {
const [playerAttributes, updatePlayerAttributes] = useState(basePlayerAttributes);
const revertPlayerAttributes = () => {
updatePlayerAttributes({...playerAttributes});
};
// Fill on first mount
useEffect(() => {
// Perform logic that uses attributes here to initialise page
}, []);
const adjustPlayerAttributes = (player, newAttributes) => {
// Update the particular attributes
playerAttributes[player] = newAttributes;
updatePlayerAttributes({...playerAttributes});
};
return (
<PlayerAttributes
playerAttributes={playerAttributes}
playerNames={["Player 1"]}
playerKeys={["player1"]}
updatePlayerAttributes={adjustPlayerAttributes} // Update the 'base' attributes, requires player key - uses function that then calls useState update function
revertPlayerAttributes={revertPlayerAttributes}
/>
);
}
// Child component one - renders another child component for each player
export const PlayerAttributes = ({
updatePlayerAttributes
}) => {
return (
<AnotherChildComponent />
);
};
// Child component 2
const AnotherChildComponent = ({ ...someThings, updatePlayerAttributes }) => {
const [editMode, updateEditMode] = useState(false); // Toggle for showing update graph elements
const [local, updateLocal] = useState({...playerAttributes}); // Store version of parent data to store changes prior to revert/save logic
// Just update the local state as the 'save' button hasn't been pressed
const updateAttributes = ( attributeGroup, attributeKey, attributeValue) => {
local[attributeGroup][attributeKey] = attributeValue;
updateLocal({...local});
};
const revertAttributes = () => {
// Need to update local back to the original state of playerAttributes
updateLocal(playerAttributes);
// revertPlayerAttributes();
updateEditMode(false); // Toggle edit mode back
};
const saveDetails = () => {
updatePlayerAttributes(playerKey, local);
updateEditMode(false);
};
return (
<FormElement
editMode={editMode}
// Should pass in a current value
playerAttributes={playerAttributes}
attributeOptions={[0.2, 0.3, 0.4, 0.5, 0.6]}
updateFunction={updateAttributes} // When you click update the local variable
/> // This handles conditional display of data depending on editMode above
);
}
// Child component 3...
const FormElement = ({ editMode, playerAttributes, attributeOptions, updateFunction, attributeGroup, attributeKey }) => {
if (editMode) {
return (
<div>
{attributeOptions.map(option =>
<div
key={option}
onClick={() => updateFunction(attributeGroup, attributeKey, option)}
>
{option)
</div>
)}
</div>
);
}
return (
<div>{//attribute of interest}</div>
);
};
I'm just a bit confused about as to why the revert button doesn't work here. My child displays and updates the information that is held in the parent but should be fully controlled through the passing of the function and variable defined in the useState call at the top of the component tree shouldn't it?
I've tried to cut down my awful code into something that can hopefully be debugged! I can't help but feel it is unnecessary complexity that is causing some issues here, or lifecycle things that I don't fully grasp. Any advice or solutions would be greatly appreciated!
Related
I have an extensive list of items in an application, so it is rendered using a virtual list provided by react-virtuoso. The content of the list itself changes based on API calls made by a separate component. What I am trying to achieve is whenever a new item is added to the list, the list automatically scrolls to that item and then highlights it for a second.
What I managed to come up with is to have the other component place the id of the newly created item inside a context that the virtual list has access to. So the virtual list looks something like this:
function MyList(props) {
const { collection } = props;
const { getLastId } useApiResultsContext();
cosnt highlightIndex = useRef();
const listRef = useRef(null);
const turnHighlightOff = useCallback(() => {
highlighIndex.current = undefined;
}, []);
useEffect(() => {
const id = getLastId();
// calling this function also resets the lastId inside the context,
// so next time it is called it will return undefined
// unless another item was entered
if (!id) return;
const index = collection.findIndex((item) => item.id === if);
if (index < 0) return;
listRef.current?.scrollToIndex({ index, align: 'start' });
highlightIndex.current = index;
}, [collection, getLastId]);
return (
<Virtuoso
ref={listRef}
data={collection}
itemContent={(index, item) => (
<ItemRow
content={item}
toHighlight={highlighIndex.current}
checkHighlight={turnHighlightOff}
/>
)}
/>
);
}
I'm using useRef instead of useState here because using a state breaks the whole thing - I guess because Virtuouso doesn't actually re-renders when it scrolls. With useRef everything actually works well. Inside ItemRow the highlight is managed like this:
function ItemRow(props) {
const { content, toHighlight, checkHighligh } = props;
const highlightMe = toHighlight;
useEffect(() => {
toHighlight && checkHighlight && checkHighligh();
});
return (
<div className={highlightMe ? 'highligh' : undefined}>
// ... The rest of the render
</div>
);
}
In CSS I defined for the highligh class a 1sec animation with a change in background-color.
Everything so far works exactly as I want it to, except for one issue that I couldn't figure out how to solve: if the list scrolls to a row that was out of frame, the highlight works well because that row gets rendered. However, if the row is already in-frame, react-virtuoso does not need to render it, and so, because I'm using a ref instead of a state, the highlight never gets called into action. As I mentioned above, using useState broke the entire thing so I ended up using useRef, but I don't know how to force a re-render of the needed row when already in view.
I kinda solved this issue. My solution is not the best, and in some rare cases doesn't highlight the row as I want, but it's the best I could come up with unless someone here has a better idea.
The core of the solution is in changing the idea behind the getLastId that is exposed by the context. Before it used to reset the id back to undefined as soon as it is drawn by the component in useEffect. Now, instead, the context exposes two functions - one function to get the id and another to reset it. Basically, it throws the responsibility of resetting it to the component. Behind the scenes, getLastId and resetLastId manipulate a ref object, not a state in order to prevent unnecessary renders. So, now, MyList component looks like this:
function MyList(props) {
const { collection } = props;
const { getLastId, resetLastId } useApiResultsContext();
cosnt highlightIndex = useRef();
const listRef = useRef(null);
const turnHighlightOff = useCallback(() => {
highlighIndex.current = undefined;
}, []);
useEffect(() => {
const id = getLastId();
resetLastId();
if (!id) return;
const index = collection.findIndex((item) => item.id === if);
if (index < 0) return;
listRef.current?.scrollToIndex({ index, align: 'start' });
highlightIndex.current = index;
}, [collection, getLastId]);
return (
<Virtuoso
ref={listRef}
data={collection}
itemContent={(index, item) => (
<ItemRow
content={item}
toHighlight={highlighIndex.current === index || getLastId() === item.id}
checkHighlight={turnHighlightOff}
/>
)}
/>
);
}
Now, setting the highlightIndex inside useEffect takes care of items outside the viewport, and feeding the getLastId call into the properties of each ItemRow takes care of those already in view.
I have two components in my project.
One is App.jsx
One is Child.jsx
In App, there is a state holding array of child state objects. All create, manage, and update of the child state is through a set function from parent.
So, Child componment doesnt have its own state. For some reason, it is my intention not to have child own state, because it matters.
However, at some points, I found it that passing data into child would be hard to manage.
Question:
So, is there a way that let the child to access the data from parent by themselves not by passing down, while having them be able to update the state like my code.
People say useContext may work, but I dont quite see how.
A example to illustrate would be prefect for the improvement.
<div id="root"></div><script src="https://unpkg.com/react#18.2.0/umd/react.development.js"></script><script src="https://unpkg.com/react-dom#18.2.0/umd/react-dom.development.js"></script><script src="https://unpkg.com/#babel/standalone#7.18.12/babel.min.js"></script>
<script type="text/babel" data-type="module" data-presets="env,react">
const {StrictMode, useState} = React;
function getInitialChildState () {
return {
hidden: false,
id: window.crypto.randomUUID(),
text: '',
};
}
function Child ({text, setText}) {
return (
<div className="vertical">
<div>{text ? text : 'Empty 👀'}</div>
<input
type="text"
onChange={ev => setText(ev.target.value)}
value={text}
/>
</div>
);
}
function ChildListItem ({state, updateState}) {
const toggleHidden = () => updateState({hidden: !state.hidden});
const setText = (text) => updateState({text});
return (
<li className="vertical">
<button onClick={toggleHidden}>{
state.hidden
? 'Show'
: 'Hide'
} child</button>
{
state.hidden
? null
: <Child text={state.text} setText={setText} />
}
</li>
);
}
function App () {
// Array of child states:
const [childStates, setChildStates] = useState([]);
// Append a new child state to the end of the states array:
const addChild = () => setChildStates(arr => [...arr, getInitialChildState()]);
// Returns a function that allows updating a specific child's state
// based on its ID:
const createChildStateUpdateFn = (id) => (updatedChildState) => {
setChildStates(states => {
const childIndex = states.findIndex(state => state.id === id);
// If the ID was not found, just return the original state (don't update):
if (childIndex === -1) return states;
// Create a shallow copy of the states array:
const statesCopy = [...states];
// Get an object reference to the targeted child state:
const childState = statesCopy[childIndex];
// Replace the child state object in the array copy with a NEW object
// that includes all of the original properties and merges in all of the
// updated properties:
statesCopy[childIndex] = {...childState, ...updatedChildState};
// Return the array copy of the child states:
return statesCopy;
});
};
return (
<div>
<h1>Parent</h1>
<button onClick={addChild}>Add child</button>
<ul className="vertical">
{
childStates.map(state => (
<ChildListItem
// Every list item needs a unique key:
key={state.id}
state={state}
// Create a function for updating a child's state
// without needing its ID:
updateState={createChildStateUpdateFn(state.id)}
/>
))
}
</ul>
</div>
);
}
const reactRoot = ReactDOM.createRoot(document.getElementById('root'));
reactRoot.render(
<StrictMode>
<App />
</StrictMode>
);
</script>
Usually context in react is used for a global things like themes and authentication. But you can use it for actions too.
const AppContext = createContext();
In App:
const getChildState = (id) => ...
const updatedChildState = (id, updatedChildState) =>
<AppContext.Provider value={{ getChildState, updatedChildState }}>...
In ChildListItem:
const { getChildState, updatedChildState } = useContext(AppContext);
const state = getChildState(id);
const setText = (text) => updatedChildState(id, { text });
You need to pass down the id anyway so ChildListItem know what to get and what to update:
<ChildListItem key={state.id} id={state.id} />
Working example
Update
Regarding your question about the theme and authentication examples let's first cite the documentation:
In a typical React application, data is passed top-down (parent to
child) via props, but such usage can be cumbersome for certain types
of props (e.g. locale preference, UI theme) that are required by many
components within an application. Context provides a way to share
values like these between components without having to explicitly pass
a prop through every level of the tree.
Examples:
Material UI uses ThemeProvider to pass down theme object. Thus all components can access the palette, typography etc.
Many apps uses context to pass down information about a currently logged in user. So all components can render accordingly.
You could try jotai atoms
App.jsx
import { atom, useAtom } from 'jotai'
export const itemAtom = atom('')
export const App = () => {
const [item] = useAtom(itemAtom)
<p>{item}</p>
...
}
Child.jsx
export const Child = () => {
const [, setItem] = useAtom(itemAtom)
<input onChange={(e) => setItem(e.value)} />
...
}
I have a Parent component which generates (and renders) a list of Child components like this:
const Parent= ({ user }) => {
const [state, setState] = useState ({selectedGames: null
currentGroup: null})
...
let allGames = state.selectedGames ? state.selectedGames.map(g => <Child
game={g} user={user} disabled={state.currentGroup.isLocked && !user.admin} />) : null
...
return ( <div> {allGames} </div>)
}
The Child component is also a stateful component, where the state (displayInfo) is only used to handle a toggle behaviour (hide/display extra data):
const Child = ({ game, user, disabled = false }) => {
const [displayInfo, setDisplayInfo] = useState(false)
const toggleDisplayInfo = () =>
{
const currentState = displayInfo
setDisplayInfo(!currentState)
}
...
<p className='control is-pulled-right'>
<button class={displayInfo ? "button is-info is-light" : "button is-info"} onClick = {toggleDisplayInfo}>
<span className='icon'>
<BsFillInfoSquareFill />
</span>
</button>
</p>
...
return ({displayInfo ? <p> Extra data displayed </p> : null})
}
When state.selectedGames is modified (for example when a user interacts with the Parent component through a select), state.selectedGames is correctly updated BUT the state of the i-th Child component stay in its previous state. As an example, let's say:
I clicked the button from the i-th Child component thus displaying "Extra data displayed" for this i-th Child only
I interact with the Parent component thus modifying state.selectedGames (no common element between current and previous state.selectedGames)
I can see that all Child have been correctly updated according to their new props (game, user, disable) but the i-th Child still has its displayInfo set to true (its state has thus not been reset contrary to what I would have (naively) expected) thus displaying "Extra data displayed".
EDIT:
After reading several SO topics on similar subjects, it appears using a key prop could solves this (note that each game passed to the Child component through the game props has its own unique ID). I have also read that such a key prop is not directly expose so I'm a bit lost...
Does passing an extra key prop with key={g.ID} to the Child component would force a re-rendering?
After a week of learning React and also diving into React Hooks, I stumbled into the problem of communicating between components like the following:
parent to child
child to parent
child to child (siblings)
I was able to communicate from a child to its own parent by adding a prop with the name onChange and passing by a function that is defined on its parent.
So this is what I had in the parent:
function handleChange(val: any) {
console.log(val)
console.log(hiddenPiece)
}
return (
<div className="board-inner">
{puzzlePieces}
</div>
)
And this is the child:
props.onChange(props.index);
The question really is, how am I able to communicate from the parent straight with its child after like a click state or when the children's state change? I have been searching for easy samples, but I guess I am not good at my research for now. I need someone who can help me out with clear examples. Thanks for taking the time to help me out here.
Here's a very basic example for the two of cases you described (parent > child & child > parent). The parent holds state, has some functions to modify it and renders two childs.
https://codesandbox.io/s/silly-browser-y9hdt?file=/src/App.tsx
const Parent = () => {
const [counter, setCounter] = useState<number>(1);
const handleIncrement = () => {
setCounter((prevCount) => prevCount + 1);
};
const handleDecrement = () => {
setCounter((prevCount) => prevCount - 1);
};
// used as prop with the children
const doubleTheCounter = () => {
setCounter((prevCount) => prevCount * 2);
};
return (
<div>
<h1>Parent counter</h1>
<p>{counter}</p>
<button onClick={handleIncrement}>+</button>
<button onClick={handleDecrement}>-</button>
<ChildTriple countFromParent={counter} />
<DoubleForParent doubleCallback={doubleTheCounter} />
</div>
);
};
The first child receives state from the parent and uses that do display something different ("triple" in this case):
type ChildTripleProps = { countFromParent: number };
// Receives count state as prop
const ChildTriple = ({ countFromParent }: ChildTripleProps) => {
const [tripleCount, setTripleCount] = useState<number>(countFromParent * 3);
useEffect(() => {
setTripleCount(countFromParent * 3);
}, [countFromParent]);
return (
<div>
<h1>Child triple counter</h1>
<p>{tripleCount}</p>
</div>
);
};
The second child receives a callback function from the parent, which changes the state at the parent:
type DoubleForParentProps = { doubleCallback: () => void };
// Receives a function as prop, used to change state of the parent
const DoubleForParent = ({ doubleCallback }: DoubleForParentProps) => {
const handleButtonClick = () => {
doubleCallback();
};
return (
<div>
<h1>Child double counter</h1>
<button onClick={handleButtonClick}>Double the parent count</button>
</div>
);
};
For your third case (child <> child) there are a lot of different options. The first one is obviously holding state in their parent and passing that down to both childs, similar to the parent in this example.
If you have grandchildren or components even further apart in the tree it probably makes sense to use some kind of state management solution. Most of the time the built in React context is totally sufficient. If you want to go for best practices regarding context I highly recommend Kent C. Dodds' blog post. This will also help you get to know the React eco system better.
External state libraries are, in my opionion, either a) too complex as a beginner, b) really new and not battle proven or c) not a best practice anymore or overblown.
The parent component contains an array of objects.
It maps over the array and returns a child component for every object, populating it with the info of that object.
Inside each child component there is an input field that I'm hoping will allow the user to update the object, but I can't figure out how to go about doing that.
Between the hooks, props, and object immutability, I'm lost conceptually.
Here's a simplified version of the parent component:
const Parent = () => {
const [categories, setCategories] = useState([]);
useEffect(()=>{
// makes an axios call and triggers setCategories() with the response
}
return(
categories.map((element, index) => {
return(
<Child
key = {index}
id = {element.id}
firstName = {element.firstName}
lastName = {element.lastName}
setCategories = {setCategories}
})
)
}
And here's a simplified version of the child component:
const Child = (props) => {
return(
<h1>{props.firstName}</h1>
<input
defaultValue = {props.lastName}
onChange={()=>{
// This is what I need help with.
// I'm a new developer and I don't even know where to start.
// I need this to update the object's lastName property in the parent's array.
}}
)
}
Maybe without knowing it, you have lifted the state: basically, instead of having the state in the Child component, you keep it in the Parent.
This is an used pattern, and there's nothing wrong: you just miss a handle function that allows the children to update the state of the Parent: in order to do that, you need to implement a handleChange on Parent component, and then pass it as props to every Child.
Take a look at this code example:
const Parent = () => {
const [categories, setCategories] = useState([]);
useEffect(() => {
// Making your AXIOS request.
}, []);
const handleChange = (index, property, value) => {
const newCategories = [...categories];
newCategories[index][property] = value;
setCategories(newCategories);
}
return categories.map((c, i) => {
return (
<Child
key={i}
categoryIndex={i}
firstName={c.firstName}
lastName={c.lastName}
handleChange={handleChange} />
);
});
}
const Child = (props) => {
...
const onInputChange = (e) => {
props.handleChange(props.categoryIndex, e.target.name, e.target.value);
}
return (
...
<input name={'firstName'} value={props.firstName} onChange={onInputChange} />
<input name={'lastName'} value={props.lastName} onChange={onInputChange} />
);
}
Few things you may not know:
By using the attribute name for the input, you can use just one handler function for all the input elements. Inside the function, in this case onInputChange, you can retrieve that information using e.target.name;
Notice that I've added an empty array dependecies in your useEffect: without it, the useEffect would have run at EVERY render. I don't think that is what you would like to have.
Instead, I guest you wanted to perform the request only when the component was mount, and that is achievable with n empty array dependecies;