useState hook in context resets unfocuses input box - reactjs

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

Related

How to add searchinputs as array in localStorage with React TypeScript

I'm trying to make a search bar, where the search will occur once the button (or enter) is clicked. To this, I want to save the searched phrases in localStorage.
I have no problem with the first part. Things work fine when searching with a button or on enter-click. However, when I try to add the search as an array to localStorage I keep getting issues (like string doesn't work with an array, and many more. I've tried A LOT of different things). I've done this with JS and vanilla React, but never with TS.
In the provided code, the search works, and to put in at least something (as simple as I could) - the latest value is also stored in localStorage.
The code can be found here
Or here:
// index file
import { useState } from "react";
import { Searchbar } from "./searchbar";
import { SearchContainer } from "./style";
export const Search = () => {
const [search, setSearch] = useState<string>("");
return (
<SearchContainer>
<Searchbar setSearch={setSearch} />
<p>SEARCHED: {search}</p>
</SearchContainer>
);
};
// search bar file
import { useRef, KeyboardEvent, useEffect } from "react";
type SearchProps = {
setSearch: React.Dispatch<React.SetStateAction<string>>;
};
export const Searchbar = ({ setSearch }: SearchProps) => {
// with useRef we don't have to reload the page for every input
const inputRef = useRef<HTMLInputElement>(null);
const handleClick = () => {
setSearch(String(inputRef.current?.value));
localStorage.setItem("form", JSON.stringify(inputRef.current?.value)); // Issue here
};
// Enable search with the enter-key
const log = (e: KeyboardEvent): void => {
e.key === "Enter" ? handleClick() : null;
};
useEffect(() => {
const value = localStorage.getItem("form");
value ? JSON.parse(value) : "";
}, [setSearch]);
return (
<div>
<input
type="text"
autoComplete="off"
ref={inputRef}
placeholder="search game..."
onKeyDown={log}
/>
<button onClick={handleClick}>SEARCH</button>
</div>
);
};
I've been on this for a while now so any help is appreciated. (I am also trying to learn TS from scratch, but until I get to here...).

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 to fire React Hooks after event

I am quite new to React and how to use hooks. I am aware that the following code doesn't work, but I wrote it to display what I would like to achieve. Basically I want to use useQuery after something changed in an input box, which is not allowed (to use a hook in a hook or event).
So how do I correctly implement this use case with react hooks? I want to load data from GraphQL when the user gives an input.
import React, { useState, useQuery } from "react";
import { myGraphQLQuery } from "../../api/query/myGraphQLQuery";
// functional component
const HooksForm = props => {
// create state property 'name' and initialize it
const [name, setName] = useState("Peanut");
const handleNameChange = e => {
const [loading, error, data] = useQuery(myGraphQLQuery)
};
return (
<div>
<form>
<label>
Name:
<input
type="text"
name="name"
value={name}
onChange={handleNameChange}
/>
</label>
</form>
</div>
);
};
export default HooksForm;
You have to use useLazyQuery (https://www.apollographql.com/docs/react/api/react-hooks/#uselazyquery) if you wan't to control when the request gets fired, like so:
import React, { useState } from "react";
import { useLazyQuery } from "#apollo/client";
import { myGraphQLQuery } from "../../api/query/myGraphQLQuery";
// functional component
const HooksForm = props => {
// create state property 'name' and initialize it
const [name, setName] = useState("Peanut");
const [doRequest, { called, loading, data }] = useLazyQuery(myGraphQLQuery)
const handleNameChange = e => {
setName(e.target.value);
doRequest();
};
return (
<div>
<form>
<label>
Name:
<input
type="text"
name="name"
value={name}
onChange={handleNameChange}
/>
</label>
</form>
</div>
);
};
export default HooksForm;
I think you could call the function inside the useEffect hook, whenever the name changes. You could debounce it so it doesn't get executed at every letter typing, but something like this should work:
handleNameChange = (e) => setName(e.target.value);
useEffect(() => {
const ... = useQuery(...);
}, [name])
So whenever the name changes, you want to fire the query? I think you want useEffect.
const handleNameChange = e => setName(e.target.value);
useEffect(() => {
// I'm assuming you'll also want to pass name as a variable here somehow
const [loading, error, data] = useQuery(myGraphQLQuery);
}, [name]);

Handling custom component checkboxes with Field as in Formik

I've been stuck for past couple of hours trying to figure out how to handle a form with a custom checkbox component in formik. I'm passing it via the <Formik as >introduced in Formik 2.0
Issue arises with input type='checkbox' as I can't just directly pass true or false values to it.
Now I'm posting the solution which I am aware is a bad implementation.
I didn't really find a way to properly pass values from the component,
so I wanted to hande it as a separate state in the component as the
checkbox will take care of its own state.
My custom input component is structured the following way
import React, { useState } from 'react';
import { StyledSwitch, Wrapper } from './Switch.styled';
type Props = {
value: boolean;
displayOptions?: boolean;
optionTrue?: string;
optionFalse?: string;
};
const Switch: React.FC<Props> = (props: Props) => {
const { value, optionTrue = 'on', optionFalse = 'off', displayOptions = false } = props;
const [switchVal, setSwitchVal] = useState<boolean>(value);
const handleSwitchChange = (): void => setSwitchVal(!switchVal);
return (
<Wrapper styledVal={switchVal}>
<StyledSwitch type="checkbox" checked={switchVal} onChange={handleSwitchChange} />
{displayOptions && (switchVal ? optionTrue : optionFalse)}
</Wrapper>
);
};
export default Switch;
The ./Switch.styled utilizes styled-components but they are not relevant to this question. Imagine them simply as an <input> and <div> respectively
Now here's the component which handles the switch
import React, { useState } from 'react';
import { Formik, Form, Field } from 'formik';
import Switch from '../../../components/forms/Switch';
const QuizMenu: React.FC = () => {
const [isMultipleChoice, setIsMultipleChoice] = useState<boolean>(false);
const sleep = (ms: number): Promise<number> => new Promise((resolve) => setTimeout(resolve, ms));
return (
<Formik
initialValues={{ isMultipleChoice: 'false', password: '' }}
onSubmit={async (values): Promise<boolean> => {
await sleep(1000);
JSON.stringify(values, null, 2);
return true;
}}
>
{
(): any => ( // to be replaced with formik destruct, but dont want eslint problems before implementation
<Form>
<div>
<Field as={Switch} onClick={setIsMultipleChoice(!isMultipleChoice)} value={isMultipleChoice === true} name="isMultipleChoice" displayOptions />
{ isMultipleChoice }
</div>
</Form>
)
}
</Formik>
);
};
export default QuizMenu
;
Which yields the following error:
Error: Maximum update depth exceeded. This can happen when a component
repeatedly calls setState inside componentWillUpdate or
componentDidUpdate. React limits the number of nested updates to
prevent infinite loops.
I also tried editing the value to string as per input type='checkboxed'but I can't really find a way to handle it. If you handle it in a separate handleChange() function you get rid of the error, but then the state doesn't update for some reason.
What would be the proper way of handling this?

useState hook always 1 step behind

I have seen other questions with the exact same title, but none solved my problem:
This is my code:
import React, { useState } from 'react';
import Validation from './Validation/Validation';
import Char from './Char/Char';
function App() {
const [textState, textSetState] = useState('');
const [charsState, charsSetState] = useState([]);
let inputChange = (event) => {
textSetState(event.target.value);
charsSetState(event.target.value.split('').map((char, index) => {
return <Char char={char} key={index} click={() => { deleteCharHandler(index) }} />
}));
}
const deleteCharHandler = (index) => {
alert(textState);
}
return (
<div className="App">
<input type="text" onChange={(event) => inputChange(event)} />
<Validation text={textState} />
{charsState}
</div>
);
}
export default App;
This is the result:
When I click a character, it displays the value from 1 step behind, like the example above.
You're putting an array of rendered Char components in your state, then rendering it. There are a number of issues with this approach.
The local copy of the state (charsState) will not be updated immediately; instead React will re-render the component with the new value for state.
Since you are defining the onClick callback within the function, deleteCharHandler will always be referencing an outdated copy of the state.
Multiple state hooks being updated in lockstep in this way will cause additional re-renders to happen.
Since the naming in your example is a bit confusing it's hard to tell what the desired behavior is to make good recommendations for how to resolve or refactor.
So there are a few things which may or may not be causing an issue so lets just clear a few things up:
You don't need to pass the anonymous function on the input:
<input type="text" onChange={inputChange} /> should suffice
As with OG state, it's never a good idea to call two setStates simultaneously, so lets combine the two:
const [state, setState] = useState({text: '', char: []});
Once you've updated everything you should be setting one state object onClick.
Your Char object is using click instead of onClick? unless you are using that as a callback method i'd switch to:
return <Char char={char} key={index} OnClick={() => deleteCharHandler(index)} />
If that doesn't fix your solution at the end, you can simply pass the deleteCharHandler the updated text value instead of re-grabbing the state value
I think you need useCallback or to pass textState in parameter. the deleteCharHandler method doesn't change in time with the textState value.
try :
return <Char char={char} key={index} click={() => { deleteCharHandler(index, textState) }} />
...
const deleteCharHandler = (index, textState) => {
alert(textState);
}
or :
import React, { useState, useCallback } from 'react';
...
const deleteCharHandler = useCallback(
(index) => {
alert(textState);
}, [textState]);
Try this
function App() {
const [textState, textSetState] = useState('');
const inputChange = useCallback((event) => {
textSetState(event.target.value);
},[])
function renderChars(){
return textState.split('').map((char, index) => {
return <Char char={char} key={index} click={() => { deleteCharHandler(index) }} />
}));
}
const deleteCharHandler = useCallback( (index) => {
alert(textState);
}, [textState])
return (
<div className="App">
<input type="text" onChange={inputChange} />
<Validation text={textState} />
{renderChars()}
</div>
);
}

Resources