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

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;

Related

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

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.

How do you rerender a React component when an object's property is updated?

I have an object in my React application which is getting updated from another system. I'd like to display the properties of this object in a component, such that it updates in real time.
The object is tracked in my state manager, Jotai. I know that Jotai will only re-render my component when the actual object itself changes, not its properties. I'm not sure if that is possible.
Here is a sample that demonstrates my issue:
import React from "react";
import { Provider, atom, useAtom } from "jotai";
const myObject = { number: 0 };
const myObjectAtom = atom(myObject);
const myObjectPropertyAtom = atom((get) => {
const obj = get(myObjectAtom)
return obj.number
});
const ObjectDisplay = () => {
const [myObject] = useAtom(myObjectAtom);
const [myObjectProperty] = useAtom(myObjectPropertyAtom);
const forceUpdate = React.useState()[1].bind(null, {});
return (
<div>
{/* This doesn't update when the object updates */}
<p>{myObject.number}</p>
{/* This doesn't seem to work at all. */}
<p>{myObjectProperty}</p>
{/* I know you shouldn't do this, its just for demo */}
<button onClick={forceUpdate}>Force Update</button>
</div>
);
};
const App = () => {
// Update the object's property
setInterval(() => {
myObject.number += 0.1;
}, 100);
return (
<Provider>
<ObjectDisplay />
</Provider>
);
};
export default App;
Sandbox
you can use useEffect for this.
useEffect(()=> {
// code
}, [myObject.number])

state not changing on onChange event in react?

This is my code:
function AddPost() {
const [file, setFile] = useState({})
const handleChange = (event) => {
setFile(event.target.files[0]);
console.log(file);
}
return (
<div>
<TextField type='file' onChange={handleChange} label='image' variant='outlined' />
</div>
)
}
I am not getting file info. on console, while i am selecting a file . Instead of that I am getting empty object why ?
You need to use a useEffect to besure you are doing your action after a state is updated. Here is an example :
import React, { Component } from "react";
import { render } from "react-dom";
const App = () => {
const [num, setNum] = React.useState(0);
const handleClick = () => {
setNum(1);
console.log('num =', num);
}
// Note the dependency array below
React.useEffect(() => console.log('num (useEffect) = ', num), [num]);
return (
<div>
<button onClick={handleClick}>Click</button>
</div>
);
};
render(<App />, document.getElementById("root"));
and here is repro on Stackblitz.
Here, on click, num will be 0 in the function, but it will be set to 1 in the useEffect.
Just the file is not updated yet (whitin onChange) , so you see the initial state
The state update is performed asynchronously and only visible in the next render call.
In the next render call, file will be updated to the new value and then your (also new) function handleChange will use that value.
This means in your current code the log will always be off by one (empty object, file 1, file 2 etc.) when uploading one file after each other.
The behavior is described in the documentation of the useState hook.
To fix the log, simply write
const newFile = event.target.files[0];
setFile(newFile);
console.log(newFile);

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.

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