REACT: How to set the state in the child and access it in the parent, receiving undefined - reactjs

I am building this project to try and improve my understanding of react :), so I am a n00b and therefore still learning the ropes of extracting components, states, props etc =)
I have a child Component DescriptionDiv, its parent component is PlusContent and finally the parent component is PlusContentHolder. The user types some input into the DescriptionDiv which then, using a props/callback passes the user input to the PlusContent.
My question/problem is: after setting useState() in the PlusContent component, I am after a button click in the PlusContentHolder component, returned with an undefined in the console.log.
How come I cannot read the useState() in the next parent component, the PlusContentHolder?
I know that useState() is async so you cannot straight up call the value of the state in the PlusContent component, but shouldn't the state value be available in the PlusContentHolder component?
below is my code for the DescriptionDiv
import './DescriptionDiv.css';
const DescriptionDiv = props => {
const onDescriptionChangeHandler = (event) => {
props.descriptionPointer(event.target.value);
}
return (
<div className='description'>
<label>
<p>Description:</p>
<input onChange={onDescriptionChangeHandler} type='text'></input>
</label>
</div>);
}
export default DescriptionDiv;
Next the code for the PlusContent comp
import React, { useState } from "react";
import DescriptionDiv from "./div/DescriptionDiv";
import ImgDiv from "./div/ImgDiv";
import "./PlusContent.css";
import OrientationDiv from "./div/OrientationDiv";
const PlusContent = (props) => {
const [classes, setClasses] = useState("half");
const [content, setContent] = useState();
const [plusContent, setPlusContent] = useState({
orientation: "left",
img: "",
description: "",
});
const onOrientationChangeHandler = (orientationContent) => {
if (orientationContent == "left") {
setClasses("half left");
}
if (orientationContent == "right") {
setClasses("half right");
}
if (orientationContent == "center") {
setClasses("half center");
}
props.orientationInfo(orientationContent);
};
const onDescriptionContentHandler = (descriptionContent) => {
props.descriptionInfo(setPlusContent(descriptionContent));
console.log(descriptionContent)
};
const onImageChangeHandler = (imageContent) => {
props.imageInfo(imageContent);
setContent(
<>
<OrientationDiv
orientationPointer={onOrientationChangeHandler}
orientationName={props.orientationName}
/> {/*
<AltDiv altPointer={onAltDivContentHandler} />
<TitleDiv titlePointer={onTitleDivContentHandler} /> */}
<DescriptionDiv descriptionPointer={onDescriptionContentHandler} />
</>
);
};
return (
<div className={classes}>
<ImgDiv imageChangeExecutor={onImageChangeHandler} />
{content}
</div>
);
};
export default PlusContent;
and lastly the PlusContentHolder
import PlusContent from "../PlusContent";
import React, { useState } from "react";
const PlusContentHolder = (props) => {
const onClickHandler = (t) => {
t.preventDefault();
descriptionInfoHandler();
};
const descriptionInfoHandler = (x) => {
console.log(x) // this console.log(x) returns and undefined
};
return (
<div>
{props.contentAmountPointer.map((content) => (
<PlusContent
orientationInfo={orientationInfoHandler}
imageInfo={imageInfoHandler}
descriptionInfo={descriptionInfoHandler}
key={content}
orientationName={content}
/>
))}
<button onClick={onClickHandler}>Generate Plus Content</button>
</div>
);
};
export default PlusContentHolder;

The reason why the descriptionInfoHandler() function call prints undefined in its console.log() statement when you click the button, is because you never provide an argument to it when you call it from the onClickHandler function.
I think that it will print the description when you type it, however. And I believe the problem is that you need to save the state in the PlusContentHolder module as well.
I would probably add a const [content, setContent] = useState() in the PlusContentHolder component, and make sure to call setContent(x) in the descriptionInfoHandler function in PlusContentHolder.
Otherwise, the state will not be present in the PlusContentHolder component when you click the button.

You need to only maintain a single state in the PlusContentHolder for orientation.
Here's a sample implementation of your use case
import React, { useState } from 'react';
const PlusContentHolder = () => {
const [orientatation, setOrientation] = useState('');
const orientationInfoHandler = (x) => {
setOrientation(x);
};
const generateOrientation = () => {
console.log('orientatation', orientatation);
};
return (
<>
<PlusContent orientationInfo={orientationInfoHandler} />
<button onClick={generateOrientation}>generate</button>
</>
);
};
const PlusContent = ({ orientationInfo }) => {
const onDescriptionContentHandler = (value) => {
// your custom implementation here,
orientationInfo(value);
};
return <DescriptionDiv descriptionPointer={onDescriptionContentHandler} />;
};
const DescriptionDiv = ({ descriptionPointer }) => {
const handleChange = (e) => {
descriptionPointer(e.target.value);
};
return <input type="text" onChange={handleChange} />;
};
I would suggest to maintain the orientation in redux so that its easier to update from the application.

SetState functions do not return anything. In the code below, you're passing undefined to props.descriptionInfo
const onDescriptionContentHandler = (descriptionContent) => {
props.descriptionInfo(setPlusContent(descriptionContent));
};
This shows a misunderstanding of the use of state. Make sure you're reading about "lifting state" in the docs.
You're also declaring needless functions, e.g. onDescriptionContentHandler in your PlusContent. The PlusContent component could just pass the descriptionInfoHandler from PlusContentHolder prop directly down to DescriptionDiv, since onDescriptionContentHandler doesn't do anything except invoke descriptionInfoHandler.
You may want to consider restructuring your app so plusContent state is maintained in PlusContentHolder, and pass that state down as props. That state would get updated when DescriptionDiv invokes descriptionInfoHandler. It'd subsequently pass the updated state down as props to PlusContent.
See my suggested flowchart.

Related

Use context for communication between components at different level

I'm building the settings pages of my apps, in which we have a common SettingsLayout (parent component) which is rended for all the settings page. A particularity of this layout is that it contains an ActionsBar, in which the submit/save button for persisting the data lives.
However, the content of this SettingsLayout is different for each page, as every one of them has a different form and a different way to interact with it. For persisting the data to the backend, we use an Apollo Mutation, which is called in one of the child components, that's why there is no access to the ActionsBar save button.
For this implementation, I thought React Context was the most appropriated approach. At the beginning, I thought of using a Ref, which was updated with the submit handler function in each different render to be aware of the changes.
I've implemented a codesandbox with a very small and reduced app example to try to illustrate and clarify better what I try to implement.
https://codesandbox.io/s/romantic-tdd-y8tpj8?file=/src/App.tsx
Is there any caveat with this approach?
import React from "react";
import "./styles.css";
type State = {
onSubmit?: React.MutableRefObject<() => void>;
};
type SettingsContextProviderProps = {
children: React.ReactNode;
value?: State;
};
type ContextType = State;
const SettingsContext = React.createContext<ContextType | undefined>(undefined);
export const SettingsContextProvider: React.FC<SettingsContextProviderProps> = ({
children
}) => {
const onSubmit = React.useRef(() => {});
return (
<SettingsContext.Provider value={{ onSubmit }}>
{children}
</SettingsContext.Provider>
);
};
export const useSettingsContext = (): ContextType => {
const context = React.useContext(SettingsContext);
if (typeof context === "undefined") {
/*throw new Error(
"useSettingsContext must be used within a SettingsContextProvider"
);*/
return {};
}
return context;
};
function ExampleForm() {
const { onSubmit } = useSettingsContext();
const [input1, setInput1] = React.useState("");
const [input2, setInput2] = React.useState("");
onSubmit.current = () => {
console.log({ input1, input2 });
};
return (
<div className="exampleForm">
<input
placeholder="Input 1"
onChange={(event) => setInput1(event.target.value)}
/>
<input
placeholder="Input 2"
onChange={(event) => setInput2(event.target.value)}
/>
</div>
);
}
function ActionsBar() {
const { onSubmit } = useSettingsContext();
return (
<section className="actionsBar">
<strong>SETTINGS</strong>
<button onClick={() => onSubmit?.current()}>Save</button>
</section>
);
}
export default function App() {
return (
<div className="App">
<SettingsContextProvider>
<ActionsBar />
<ExampleForm />
</SettingsContextProvider>
</div>
);
}
The main caveat I see in this approach is that you change the whole submit function when you need only reaction to submit event. Event is the catch, I think.
Your approach works ok, but has no extension points, for cases such as validation etc.
So I propose to use EventEmitter in any form (better with types support) as a context value e.g. communication channel.
This is a fork of your codesandbox that illustrates this approach:
https://codesandbox.io/s/friendly-fog-qlrusj?file=/src/App.tsx

input value not updating when mutating state

While creating a little project for learning purposes I have come across an issue with the updating of the input value. This is the component (I have tried to reduce it to a minimum).
function TipSelector({selections, onTipChanged}: {selections: TipSelectorItem[], onTipChanged?:(tipPercent:number)=>void}) {
const [controls, setControls] = useState<any>([]);
const [tip, setTip] = useState<string>("0");
function customTipChanged(percent: string) {
setTip(percent);
}
//Build controls
function buildControls()
{
let controlList: any[] = [];
controlList.push(<input className={styles.input} value={tip.toString()} onChange={(event)=> {customTipChanged(event.target.value)}}></input>);
setControls(controlList);
}
useEffect(()=>{
console.log("TipSelector: useEffect");
buildControls();
return ()=> {
console.log("unmounts");
}
},[])
console.log("TipSelector: Render -> "+tip);
return (
<div className={styles.tipSelector}>
<span className={globalStyles.label}>Select Tip %</span>
<div className={styles.btnContainer}>
{
controls
}
</div>
</div>
);
}
If I move the creation of the input directly into the return() statement the value is updated properly.
I'd move your inputs out of that component, and let them manage their own state out of the TipSelector.
See:
https://codesandbox.io/s/naughty-http-d38w9
e.g.:
import { useState, useEffect } from "react";
import CustomInput from "./Input";
function TipSelector({ selections, onTipChanged }) {
const [controls, setControls] = useState([]);
//Build controls
function buildControls() {
let controlList = [];
controlList.push(<CustomInput />);
controlList.push(<CustomInput />);
setControls(controlList);
}
useEffect(() => {
buildControls();
return () => {
console.log("unmounts");
};
}, []);
return (
<div>
<span>Select Tip %</span>
<div>{controls}</div>
</div>
);
}
export default TipSelector;
import { useState, useEffect } from "react";
function CustomInput() {
const [tip, setTip] = useState("0");
function customTipChanged(percent) {
setTip(percent);
}
return (
<input
value={tip.toString()}
onChange={(event) => {
customTipChanged(event.target.value);
}}
></input>
);
}
export default CustomInput;
You are only calling buildControls once, where the <input ... gets its value only that single time.
Whenever React re-renders your component (because e.g. some state changes), your {controls} will tell React to render that original <input ... with the old value.
I'm not sure why you are storing your controls in a state variable? There's no need for that, and as you noticed, it complicates things a lot. You would basically require a renderControls() function too that you would replace {controls} with.

Set React Context inside function-only component

My goal is very simple. I am just looking to set my react context from within a reusable function-only (stateless?) react component.
When this reusable function gets called it will set the context (state inside) to values i provide. The problem is of course you can't import react inside a function-only component and hence I cannot set the context throughout my app.
There's nothing really to show its a simple problem.
But just in case:
<button onCLick={() => PlaySong()}></button>
export function PlaySong() {
const {currentSong, setCurrentSong} = useContext(StoreContext) //cannot call useContext in this component
}
If i use a regular react component, i cannot call this function onClick:
export default function PlaySong() {
const {currentSong, setCurrentSong} = useContext(StoreContext) //fine
}
But:
<button onCLick={() => <PlaySong />}></button> //not an executable function
One solution: I know i can easily solve this problem by simply creating a Playbtn component and place that in every song so it plays the song. The problem with this approach is that i am using a react-player library so i cannot place a Playbtn component in there...
You're so close! You just need to define the callback inside the function component.
export const PlaySongButton = ({...props}) => {
const {setCurrentSong} = useContext(StoreContext);
const playSong = () => {
setCurrentSong("some song");
}
return (
<button
{...props}
onClick={() => playSong()}
/>
)
}
If you want greater re-usability, you can create custom hooks to consume your context. Of course where you use these still has to follow the rules of hooks.
export const useSetCurrentSong = (song) => {
const {setCurrentSong} = useContext(StoreContext);
setCurrentSong(song);
}
It is possible to trigger a hook function by rendering a component, but you cannot call a component like you are trying to do.
const PlaySong = () => {
const {setCurrentSong} = useContext(StoreContext);
useEffect( () => {
setCurrentSong("some song");
}, []
}
return null;
}
const MyComponent = () => {
const [shouldPlay, setShouldPlay] = useState(false);
return (
<>
<button onClick={() => setShouldPlay(true)}>Play</button>
{shouldPlay && <PlaySong />}
</>
)
}

How can I update a hook from one component and have it update another component as a result?

I'm trying to create a hook with a state and use that as the common source for multiple components.
What I've tried:
import React, {useState} from 'react';
/**
* Example of Hook that holds state, which should update other components when it changes
*/
const useExternalHookAsState = () => {
const [message, setMessage] = useState('nothing yet');
const updateMessage = (newMessage) => {
console.log('message should be update to..', newMessage);
setMessage(newMessage);
}
return [message, updateMessage];
};
/**
* Example component for updating the state
*/
const MessageUpdater = () =>
{
const [message, updateMessage] = useExternalHookAsState();
return (
<div>
<input type="text" value={message} onChange={(e) => updateMessage(e.target.value)} />
</div>
);
};
/**
* Expecting the message to be updated in here as well when the updateMessage is triggered, but that doens't happen
*/
const App = () =>
{
const [message] = useExternalHookAsState();
useEffect(()=> {
console.log('effect on message tirggered');
}, [message]);
return (
<div>
<p>message is.. {message}</p>
<MessageUpdater />
</div>
);
};
export default App;
Expected behavior: I was expecting that the new {message} will be updated in the App component but that just stays the same as 'nothing yet', and useEffect on the message doens't get triggered.
Am I using hooks in a wrong way? How can I achieve this behavior?
Your message inside App is not updated because you use useExternalHookAsState twice. The first time inside the MessageHandler, and the second time in App component. And they have different states. This way you only use setMessage in the child component of MessageHandler, which causes only that child component to be updated. And the App is not updated.
To solve this problem, raise the state and state management to the component in which you want to receive its updates. In your case, this is App. And then, through props, just pass the value and setter to the MessageHandler component.
The behavior you wanted to achieve is shown below:
import React, {useState, useEffect} from "react";
const useExternalHookAsState = () => {
const [message, setMessage] = useState('nothing yet');
const updateMessage = (newMessage) => {
console.log('message should be update to..', newMessage);
setMessage(newMessage);
}
return [message, updateMessage];
};
const MessageUpdater = ({message, onUpdate}) => {
return (
<div>
<input type="text" value={message} onChange={(e) => onUpdate(e.target.value)}/>
</div>
);
};
const App = () => {
const [message, updateMessage] = useExternalHookAsState();
useEffect(() => {
console.log('effect on message tirggered');
}, [message]);
return (
<div>
<p>message is.. {message}</p>
<MessageUpdater message={message} onUpdate={updateMessage}/>
</div>
);
};
export default App;
Though the answer from xom9ikk is right (calling a hook twice will result in each hook having a different state, so one hook can't hold a global state as I've assumed), I have chosen to use the context approach as this doesn't require passing props and results in more clean code when working with a larger codebase.
The end example result that I've used is:
import React, {useState, useEffect, useContext} from 'react';
/**
* Initializing Context
*/
const MessageContext = React.createContext();
/**
* Creating provider with default state
* - holds the state for the message used everywhere in the App
* - takes children parameter because it needs to render the children of the context
* - updateMessage can be used from any child of provider and will update the global state
*/
const MessageProvider = ({children}) => {
const [message, setMessage] = useState('nothing yet');
const updateMessage = (newMessage) => {
setMessage(newMessage);
}
return (
<MessageContext.Provider value={[message, updateMessage]}>
{children}
</MessageContext.Provider>
)
}
/**
* Example component for updating the state
*/
const MessageUpdater = () =>
{
const [message, updateMessage] = useContext(MessageContext);
return (
<div>
<p>message in message updater is.. {message}</p>
<input type="text" value={message} onChange={(e) => updateMessage(e.target.value)} />
</div>
);
};
/**
* Example of component that displays the message
* (all child components can use the message in the same way, without passing props)
*/
const App = () =>
{
const [message] = useContext(MessageContext);
useEffect(()=> {
console.log('effect on message tirggered');
}, [message]);
return (
<>
<p>Message in app is.. {message}</p>
<MessageUpdater />
</>
);
};
/**
* Wrapps the App with the provider that holds the global message state and update function
*/
const AppContext = () =>
{
return (
<MessageProvider>
<App />
</MessageProvider>
)
}
export default AppContext;

useState hook in context resets unfocuses input box

My project takes in a display name that I want to save in a context for use by future components and when posting to the database. So, I have an onChange function that sets the name in the context, but when it does set the name, it gets rid of focus from the input box. This makes it so you can only type in the display name one letter at a time. The state is updating and there is a useEffect that adds it to local storage. I have taken that code out and it doesn't seem to affect whether or not this works.
There is more than one input box, so the auto focus property won't work. I have tried using the .focus() method, but since the Set part of useState doesn't happen right away, that hasn't worked. I tried making it a controlled input by setting the value in the onChange function with no changes to the issue. Other answers to similar questions had other issues in their code that prevented it from working.
Component:
import React, { useContext } from 'react';
import { ParticipantContext } from '../../../contexts/ParticipantContext';
const Component = () => {
const { participant, SetParticipantName } = useContext(ParticipantContext);
const DisplayNameChange = (e) => {
SetParticipantName(e.target.value);
}
return (
<div className='inputBoxParent'>
<input
type="text"
placeholder="Display Name"
className='inputBox'
onChange={DisplayNameChange}
defaultValue={participant.name || ''} />
</div>
)
}
export default Component;
Context:
import React, { createContext, useState, useEffect } from 'react';
export const ParticipantContext = createContext();
const ParticipantContextProvider = (props) => {
const [participant, SetParticipant] = useState(() => {
return GetLocalData('participant',
{
name: '',
avatar: {
name: 'square',
imgURL: 'square.png'
}
});
});
const SetParticipantName = (name) => {
SetParticipant({ ...participant, name });
}
useEffect(() => {
if (participant.name) {
localStorage.setItem('participant', JSON.stringify(participant))
}
}, [participant])
return (
<ParticipantContext.Provider value={{ participant, SetParticipant, SetParticipantName }}>
{ props.children }
</ParticipantContext.Provider>
);
}
export default ParticipantContextProvider;
Parent of Component:
import React from 'react'
import ParticipantContextProvider from './ParticipantContext';
import Component from '../components/Component';
const ParentOfComponent = () => {
return (
<ParticipantContextProvider>
<Component />
</ParticipantContextProvider>
);
}
export default ParentOfComponent;
This is my first post, so please let me know if you need additional information about the problem. Thank you in advance for any assistance you can provide.
What is most likely happening here is that the context change is triggering an unmount and remount of your input component.
A few ideas off the top of my head:
Try passing props directly through the context provider:
// this
<ParticipantContext.Provider
value={{ participant, SetParticipant, SetParticipantName }}
{...props}
/>
// instead of this
<ParticipantContext.Provider
value={{ participant, SetParticipant, SetParticipantName }}
>
{ props.children }
</ParticipantContext.Provider>
I'm not sure this will make any difference—I'd have to think about it—but it's possible that the way you have it (with { props.children } as a child of the context provider) is causing unnecessary re-renders.
If that doesn't fix it, I have a few other ideas:
Update context on blur instead of on change. This would avoid the context triggering a unmount/remount issue, but might be problematic if your field gets auto-filled by a user's browser.
Another possibility to consider would be whether you could keep it in component state until unmount, and set context via an effect cleanup:
const [name, setName] = useState('');
useEffect(() => () => SetParticipant({ ...participant, name }), [])
<input value={name} onChange={(e) => setName(e.target.value)} />
You might also consider setting up a hook that reads/writes to storage instead of using context:
const useDisplayName = () => {
const [participant, setParticipant] = useState(JSON.parse(localStorage.getItem('participant') || {}));
const updateName = newName => localStorage.setItem('participant', {...participant, name} );
return [name, updateName];
}
Then your input component (and others) could get and set the name without context:
const [name, setName] = useDisplayName();
<input value={name} onChange={(e) => setName(e.target.value)} />

Resources