How to fire React Hooks after event - reactjs

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]);

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.

useInput custom hook with redux state

I'm trying to use a redux store state value in an input, but I have a custom useInput hook, that I can't figure how to make them work together.
I built a react custom useInput hook that handles the value, and change/blur events. Used like:
const {
value: titleValue,
error: titleError,
inputChangedHandler: titleChangedHandler,
inputBlurHandler: titleBlurHandler,
setValue: setTitleValue,
} = useInput(validateTitle);
<input
error={titleError ?? false}
id="title"
name="title"
type="text"
placeholder="Stream Title"
autoComplete="off"
value={titleValue}
onChange={titleChangedHandler}
onBlur={titleBlurHandler}
/>
my problem is that if I want to use it in an 'edit' components, which I want to fetch the initial values from an existing state, I cannot do it, because the input value is bound to the useInput value property.
so I can't do this, with my useInput custom hook:
const selectedItem = useSelector((state) => state.items.selectedItem);
<input
error={titleError ?? false}
id="title"
name="title"
type="text"
placeholder="Stream Title"
autoComplete="off"
value={selectedItem.title} <-- use the state selectedItem value
onChange={titleChangedHandler}
onBlur={titleBlurHandler}
/>
my useInput customer hook is just in charge of validation of the input value. It would work well if I could initially set the value to the store value, but my component is using useEffect to call an API getById(id) so the first time the component loads there is still no selectedItem, so I cannot initially set the useInput to the selectedItem.title.
this is my useInput custom hook code:
import { useState } from 'react';
const useInput = (validate) => {
console.log('in useInput');
const [value, setValue] = useState('');
const [isTouched, setIsTouched] = useState(false);
const validationResult = validate(value);
const error = !validationResult.isValid && isTouched && validationResult.message;
const inputChangedHandler = (event) => {
setIsTouched(true);
setValue(event.target.value);
};
const inputBlurHandler = () => {
setIsTouched(true);
};
return { value, error, inputChangedHandler, inputBlurHandler, setValue };
};
export default useInput;
How can I fix it?

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

How to connect redux store using useSelector() when input fields already mapped using useState()

I am playing around with the new React-Redux Hooks library
I have an react component that has two input fields that update to the react store using useState() - desc and amount. In order to update changes to the the redux store when field has been edited I use onBlur event and call dispatch to the redux store. That works fine.
When I want to clear the fields from another component I would like this to work in same manner as for class based functions via connect & map State to Props, however to to this with functional component I need to utilise useSelector(). I cannot do this as the identifiers desc and amount are already used by useState()
What am I missing here?
import { useDispatch, useSelector } from "react-redux"
import { defineItem, clearItem } from "../store/actions"
const ItemDef = props => {
const dispatch = useDispatch()
const [desc, setDesc] = useState(itemDef.desc)
const [amount, setAmount] = useState(itemDef.amount)
//MAPSTATETOPROPS
//I WANT TO HAVE THESE VALUES UPDATED WHEN REDUX STORE CHANGES FROM ANOTHER COMPONENT
//THESE LINES WILL CAUSE ERROR to effect - identifier has already been declared
const desc = useSelector(state => state.pendingItem.desc)
const amount = useSelector(state => state.pendingItem.amount)
return (
<div>
<p>Define new items to be added below - before clicking Add Item</p>
<input
value={desc}
type="text"
name="desc"
placeholder="Description of Item"
onChange={e => setDesc(e.target.value)}
//Use onBlur Event so that changes are only submitted to store when field loses focus
onBlur={e => dispatch(defineItem(desc, amount))}
/>
<input
value={amount}
type="number"
name="amount"
placeholder="Amount"
onChange={e => setAmount(e.target.value)}
//Use onBlur Event so that changes are only submitted to store when field loses focus
onBlur={e => {
dispatch(defineItem(desc, amount))
}}
/>
</div>
)
}
export default ItemDef
SOLUTION - WITH FULL CODE IN REPOSITORY
I worked out a solution by using useSelector (to map pendingItem part of redux state to itemDef) and the setEffect hook to apply useState to either state item (from input) or itemDef (from Redux State - this happens when redux is updated by another component or through the ADD ITEM TO INPUT button)
I have posted the working component below. I have also posted this small application to demonstrate how to use reacdt-redux libraries with both class based components and fuinctional components using hooks
The repository is https://github.com/Intelliflex/hiresystem
//**************************************************************************************************
//***** ITEMDEF COMPONENT - Allow entry of new Items (dispatched from button in HireList Table) ****
//**************************************************************************************************
import React, { useState, useEffect, useRef } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { defineItem, clearItem } from '../store/actions'
import _ from 'lodash'
const ItemDef = props => {
//BRING IN DISPATCH FROM REDUX STORE
const dispatch = useDispatch()
//DEFINE SELECTOR - EQUIV TO MAPSTATETOPROPS
const { itemDef } = useSelector(state => ({
itemDef: state.pendingItem
}))
const [item, setItem] = useState({ desc: '', amount: 0 })
const onChange = e => {
setItem({
...item,
[e.target.name]: e.target.value
})
}
const prevItem = useRef(item)
useEffect(() => {
//WE NEED TO CONDITIONALLY UPDATE BASED ON EITHER STORE BEING CHANGED DIRECTLY OR INPUT FORM CHANGING
if (!_.isEqual(item, prevItem.current)) {
//INPUT HAS CHANGED
setItem(item)
} else if (!_.isEqual(item, itemDef)) {
//REDUX STATE HAS CHANGED
setItem(itemDef)
}
prevItem.current = item
}, [item, itemDef]) //Note: item and ItemDef are passed in as second argument in order to use setItem
const clearIt = e => {
dispatch(clearItem())
}
const addIt = e => {
dispatch(defineItem({ desc: 'MY NEW ITEM', amount: 222 }))
}
return (
<div>
<p>Define new items to be added below - before clicking Add Item</p>
<input
value={item.desc}
type='text'
name='desc'
placeholder='Description of Item'
onChange={onChange}
//Use onBlur Event so that changes are only submitted to store when field loses focus
onBlur={e => dispatch(defineItem(item))}
/>
<input
value={item.amount}
type='number'
name='amount'
placeholder='Amount'
onChange={onChange}
//Use onBlur Event so that changes are only submitted to store when field loses focus
onBlur={e => dispatch(defineItem(item))}
/>
<button onClick={clearIt}>CLEAR ITEM</button>
<button onClick={addIt}>ADD ITEM TO INPUT</button>
</div>
)
}
export default ItemDef

Resources