react useState update not triggering with custom hook - reactjs

I made a custom hook for my player like this
const usePlayer = () => {
const [volume, setVolume] = useState<number>(100);
const playerRef = useRef<HTMLAudioElement | null>(null);
const [playing, setPlaying] = useState<boolean>(false);
useEffect(() => {
console.log(volume);
}, [volume]);
return {
playing,
volume,
setPlaying,
setVolume,
playerRef,
};
};
export default usePlayer;
I am listening for changes in another component which uses the userPlayer()
const PlayerButton = () => {
const { playerRef, volume } = usePlayer();
useEffect(() => {
console.log(volume);
if (playerRef.current !== null) {
playerRef.current.volume = volume / 100;
}
}, [volume]);
return (
<>
<div className="lg:mr-2 flex align-middle">
<audio ref={playerRef} preload="none">
<source src="" type="audio/ogg"></source>
<source src="" type="audio/mp3"></source>
</audio>
</div>
</>
);
};
export default PlayerButton
the setVolume is called like this in another component
<input
type="range"
className="w-full"
value={volume}
onChange={(e) => setVolume(parseInt(e.target.value))}
min={0}
max={100}
/>
But the useEffect does only trigger inside the usePlayer component and not in the second component who uses the usePlayer()
any idea why ?

If you use setVolume in PlayerButton component, it will trigger the useEffect in PlayerButton.
It will not trigger the useEffect if you are using setVolume in a different component.
Do two components using the same Hook share state? No. Custom Hooks
are a mechanism to reuse stateful logic (such as setting up a
subscription and remembering the current value), but every time you
use a custom Hook, all state and effects inside of it are fully
isolated.
How does a custom Hook get isolated state? Each call to a Hook gets
isolated state. Because we call useFriendStatus directly, from React’s
point of view our component just calls useState and useEffect. And as
we learned earlier, we can call useState and useEffect many times in
one component, and they will be completely independent.
https://reactjs.org/docs/hooks-custom.html
If you want 2 components to share state, you need to use redux or useContext to manage the state in a store.
https://reactjs.org/docs/hooks-reference.html#usecontext
Example use of useContext and Provider.
export const PlayerContext = createContext();
export const usePlayer = () => {
const context = useContext(PlayerContext);
if (!context && typeof window !== 'undefined') {
throw new Error(`usePlayer must be used within a PlayerContext `);
}
return context;
};
export const PlayerProvider = ({ children }) => {
const [ volume, setVolume ] = useState(100);
return <PlayerContext.Provider value={{ volume, setVolume }}>{children}</PlayerContext.Provider>
}
You need to wrap your app with the provider like this.
<PlayerProvider><App /></PlayerProvider>
Then use it like this
const { volume, setVolume } = usePlayer()

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.

React Context is not working as expected: Unable to change the value of shared variable

I made a context to share the value of the variable "clicked" throughout my nextjs pages, it seems to give no errors but as you can see the variable's value remains FALSE even after the click event. It does not change to TRUE. This is my first time working with context, what am I doing wrong?
I'm using typescript
PS: After the onClick event the log's number shoots up by 3 or 4, is it being executed more than once, but how?
controlsContext.tsx
import { createContext, FC, useState } from "react";
export interface MyContext {
clicked: boolean;
changeClicked?: () => void;
}
const defaultState = {
clicked: false,
}
const ControlContext = createContext<MyContext>(defaultState);
export const ControlProvider: FC = ({ children }) => {
const [clicked, setClicked] = useState(defaultState.clicked);
const changeClicked = () => setClicked(!clicked);
return (
<ControlContext.Provider
value={{
clicked,
changeClicked,
}}
>
{children}
</ControlContext.Provider>
);
};
export default ControlContext;
Model.tsx
import ControlContext from "../contexts/controlsContext";
export default function Model (props:any) {
const group = useRef<THREE.Mesh>(null!)
const {clicked, changeClicked } = useContext(ControlContext);
const handleClick = (e: MouseEvent) => {
//e.preventDefault();
changeClicked();
console.log(clicked);
}
useEffect(() => {
console.log(clicked);
}, [clicked]);
useFrame((state, delta) => (group.current.rotation.y += 0.01));
const model = useGLTF("/scene.gltf ");
return (
<>
<TransformControls enabled={clicked}>
<mesh
ref={group}
{...props}
scale={clicked ? 0.5 : 0.2}
onClick={handleClick}
>
<primitive object={model.scene}/>
</mesh>
</TransformControls>
</>
)
}
_app.tsx
import {ControlProvider} from '../contexts/controlsContext';
function MyApp({ Component, pageProps }: AppProps) {
return (
<ControlProvider>
<Component {...pageProps}
/>
</ControlProvider>
)
}
export default MyApp
Issues
You are not actually invoking the changeClicked callback.
React state updates are asynchronously processed, so you can't log the state being updated in the same callback scope as the enqueued update, it will only ever log the state value from the current render cycle, not what it will be in a subsequent render cycle.
You've listed the changeClicked callback as optional, so Typescript will warn you if you don't use a null-check before calling changeClicked.
Solution
const { clicked, changeClicked } = useContext(ControlContext);
...
<mesh
...
onClick={(event) => {
changeClicked && changeClicked();
}}
>
...
</mesh>
...
Or declare the changeClicked as required in call normally. You are already providing changeClicked as part of the default context value, and you don't conditionally include in in the provider, so there's no need for it to be optional.
export interface MyContext {
clicked: boolean,
changeClicked: () => void
}
...
const { clicked, changeClicked } = useContext(ControlContext);
...
<mesh
...
onClick={(event) => {
changeClicked();
}}
>
...
</mesh>
...
Use an useEffect hook in to log any state updates.
const { clicked, changeClicked } = useContext(ControlContext);
useEffect(() => {
console.log(clicked);
}, [clicked]);
Update
After working with you and your sandbox it needed a few tweaks.
Wrapping the index.tsx JSX code with the ControlProvider provider component so there was a valid context value being provided to the app. The UI here had to be refactored into a React component so it could itself also consume the context value.
It seems there was some issue with the HTML canvas element, or the mesh element that was preventing the Modal component from maintaining a "solid" connection with the React context. It wasn't overtly clear what the issue was here, but passing the context values directly to the Modal component as props resolved the issue with the changeClicked callback becoming undefined.
A few things -
setClicked((prev) => !prev);
instead of
setClicked(!clicked);
As it ensures it's not using stale state. Then you are also doing -
changeClicked
But it should be -
changeClicked();
Lastly, you cannot console.log(clicked) straight after calling the set state function, it will be updated in the next render

useState not updating[NOT AN ASYNC ISSUE]

I'm new to react hooks so I'm practicing with showing and hiding a div when checking and unckecking a checkbox input. The problem is that the state updates on the main file where I have the function that handles it but in the file where I actually have the div it does not update so it does not hide or display.
File that handles the change of the state:
import {react, useState} from "react";
export const Checker = () => {
const [checked, setChecked] = useState(true)
const clickHandler = () => {
setChecked(!checked)
console.log(checked)
}
return {checked, clickHandler, setChecked}
}
File where the checkbox is located:
import React from "react";
import { Extras, Wrapper } from "./extras.styles";
import { Checker } from "../hooks/useCheckboxes";
const Extra = () => {
const {checked, setChecked, clickHandler} = Checker()
return <>
<Extras>
<Wrapper>
<input type= 'checkbox' onClick={clickHandler} checked = {checked} onChange={e => setChecked(e.target.checked)}></input>
</Wrapper>
</Extras>
</>
}
export default Extra;
File that contains the div i want to display and hide dynamically:
import house from '../../icons/house.png'
import { Wrapper } from "./foto.styles";
import { Checker } from "../hooks/useCheckboxes";
import { Inside, Middle} from "./foto.styles";
const Home = () => {
const {checked} = Checker()
return <>
<Wrapper>
<Inside>
<Middle>
{checked && <House src={house}/>}
</Middle>
</Inside>
</Wrapper>
</>
}
export default Home;
Some issues are:
Checker looks like you want it to be a custom hook, not a React component, so it should be called useChecker or something like that, not Checker
You have both a change handler and a click handler. You should only have one. If you want the new state to come from the checkbox, you should use e.target.checked. If you want the new state to flip the old state, use the clickHandler you defined in Checker.
You only need a fragment when enclosing multiple elements. If you only have one, you don't need a fragment.
Because state setters don't update the variable immediately, your console.log(checked) won't display the new value, but the old value - if you want to log the new value when it changes, use useEffect with a dependency array of [checked] instead.
const Extra = () => {
const { checked, clickHandler } = useChecker()
return (
<Extras>
<Wrapper>
<input type='checkbox'checked={checked} onChange={clickHandler} />
</Wrapper>
</Extras>
)
}
use it like that
const {checked, clickHandler, setChecked} = Checker()
Or if you want to be able to make custom names then you need to use an array instead of an object.
the function return value.
return [checked, clickHandler, setChecked]
the function call
const [checked, setChecked, clickHandler] = Checker()
and for convention follow react hooks naming rules by renaming the function to useChecker() instead of Checker()

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

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