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

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

Related

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?

React Hooks: State Is Not Updating in Function

I have spent several hours trying to fix this issue with no luck. I have a reusable component that starts with a state of any empty object, the object is given a series of properties based off of props, and then based on user input on each set of radio buttons will show a child component.
Unfortunately, the function that is supposed to update the state that will then trigger whether the user sees the child component is not working.
I believe the issue is in my editDisplay function. Currently when I click one of the radio buttons the new object that should become the new state is logged to the console correctly, but when I use React Devtools to inspect the page, I see that state is not updating. That being said, I could see a case where I am misunderstanding useEffect, and perhaps useEffect is running each time editDisplay is running. Any help would be appreciated.
import React, {useState, useEffect} from 'react'
import {Form} from 'react-bootstrap'
import RatingInput from './RatingInput'
export default function MembershipForm({inputs, name, record, setRecord}) {
const [display, setDisplay] = useState({})
useEffect(()=>{
inputs.forEach(input => {
var newDisplay = display
newDisplay[name+input]=false
setDisplay(newDisplay)})
},[])
const editDisplay = (input, visible) => {
var newDisplay =display
newDisplay[name+input]=visible
console.log(newDisplay)
setDisplay(newDisplay)
}
const questions=inputs.map(input => {
return (
<div key={name+input}>
<Form.Group>
<Form.Label className='mx-2'>Do you have a {name} {input} Rating?</Form.Label>
<Form.Check inline label='Yes' value={true} name={name+input} type='radio' className = 'mx-2' onClick ={() =>{editDisplay(input, true)}} />
<Form.Check inline label='No' value ={false} name={name+input} type='radio' className = 'mx-2' defaultChecked onClick = {() =>{editDisplay(input, false)}} />
</Form.Group>
{display[name+input] && <RatingInput input={input}/>}
</div >
)
})
return (
<div className='ml-3'>
{questions}
</div>
)
}
const editDisplay = (input, visible) => {
var newDisplay = display
newDisplay[name+input]=visible
console.log(newDisplay)
setDisplay(newDisplay)
}
You should perform react state update in an immutable way.
What you are doing above is you are mutating existing state variable (that assignment on first line doesn't add much: newDisplay refers to same object as display). In such case react might not detect change. Do this instead:
var newDisplay = { ... display };

React Hooks with Styled Components - Form not working

So I'm using React Hooks with styled components, I have tried to style a form but when I make it into a styled component the form doesn't work ie you type one letter then the form looses focus, you have to click back into the input box, type one letter and it looses focus again etc...
I'm also getting this warning in the dev tools but I don't really understand what I need to do -
index.js:27 The component styled.form with the id of "sc-eCssSg" has been created dynamically.
You may see this warning because you've called styled inside another component.
To resolve this only create new StyledComponents outside of any render method and function component.
at LocationForm (https://3deis.csb.app/src/Components/LocationForm.js:31:39)
at div
at App (https://3deis.csb.app/src/App.js:53:39)
How do I change the below code to do what is needed to make it work ?
import React, { useState } from "react";
import axios from "axios";
import styled from "styled-components";
const LocationForm = (props) => {
const [locationName, setName] = useState("");
const Form = styled.form``;
const handleSubmit = (evt) => {
evt.preventDefault();
axios
.get(
`http://www.mapquestapi.com/geocoding/v1/address?key=z2G40AM2VSDfXx7MQtCqAvmXmoYEX8cV&location=${locationName}&maxResults=1`
)
.then((res) => {
const latitude = res.data.results[0].locations[0].displayLatLng.lat;
const longitude = res.data.results[0].locations[0].displayLatLng.lng;
const city = res.data.results[0].locations[0].adminArea5;
// const submitted = !true;
props.callbackFromParent(
locationName,
// submitted,
latitude,
longitude,
city
);
})
.catch((error) => {
console.log(error);
});
};
return (
<Form onSubmit={handleSubmit}>
<label>
Location:
<input
type="text"
value={locationName}
onChange={(e) => setName(e.target.value)}
/>
</label>
<input type="submit" value="Submit" />
</Form>
);
};
export default LocationForm;
It says exactly what is happening. You are creating const Form = styled.form inside your LocationForm render function. If you move it 4 lines up outsie the function it will stop giving the warning. In general you should never create a styled component inside a render function, because it will recreate the form each render (so each time you input a character) instead of only once upon initialization.

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

Resources