How to update deep objects in a nest react - reactjs

I need help making a handler that will update the nested object state.
Here is the sample code
const [state, setState] = useState({
firstName: '',
lastName: '',
permanentAddress: {
street: '',
city: ''
},
currentAddress: {
street: '',
city: ''
}
});
const handler = (e) => {
setState({...state, [e.target.name]: e.target.value})
}
Then I import a custom textfield
export const CustomTextField = (props) => {
const { handler, ...rest } = props
return (
<TextField {...rest} onChange={handler} />
)
}
Now I know this will only create a new key named street with the value instead of updating the one already in the state. I have also thought of just adding a prop that will indicate whether it is nested, something like
<CustomTextField cat='permanentAddress' name='street' handler={handler} />
Then I will change the handler to something like this, passing along cat in the parameter
const handler = (cat, e) => {
if (cat) {
const category = {...state[cat]}
category[e.target.name] = e.target.value
setState({...state, [cat]: category})
} else {
//non-nested update here
}
}
Is there a better way to do this? Thank you!

Related

Testing React Context update

I have been struggling to test a React Context update for a while. I can predefine the value of the Context.Provider however, the solution is not ideal because an update to the context which is supposed to happen within component utilising the context is not actually happening.
When I test this manually the text 'Account name: abc' changes to 'Account name: New account name' but not in the test. The context value remains the same.
The reason I predefine the value is because the component relies on a fetch response from a parent and I am unit testing <ImportAccounts /> only.
In a test I am rendering a component with predefined value
test('accountName value is updated on click', () => {
const { getByText, getByLabelText, getByRole } = render(
<ImportAccountsContext.Provider value={{ accountName: { value: 'abc', setValue: jest.fn() }}}>
<ImportAccounts />
</ImportAccountsContext.Provider>,
);
expect(getByText('Account name: abc')).toBeInTheDocument();
const input = getByLabelText('Account name');
fireEvent.change(input, { target: { value: 'New account name' } });
fireEvent.click(getByRole('button', { name: 'Update' }));
expect(getByText('Account name: New account name')).toBeInTheDocument();
});
Here's my context
import React, { createContext, useContext, useState, useCallback, Dispatch, SetStateAction } from 'react';
export interface StateVariable<T> {
value: T;
setValue: Dispatch<SetStateAction<T>>;
}
export interface ImportAccountsState {
accountName: StateVariable<string>;
}
export const ImportAccountsContext = createContext<ImportAccountsState>(
{} as ImportAccountsState,
);
export const ImportAccountsProvider = ({ children }: { children: React.ReactNode }) => {
const [accountName, setAccountName] = useState('');
const initialState: ImportAccountsState = {
accountName: {
value: accountName,
setValue: setAccountName,
},
};
return (
<ImportAccountsContext.Provider value={initialState}>
{children}
</ImportAccountsContext.Provider>
);
};
export const useImportAccountsContext = () => {
return useContext<ImportAccountsState>(ImportAccountsContext);
};
Import Accounts is as simple as
export const ImportAccounts = () => {
const { accountName } = useImportAccountsContext();
const [newAccountName, setNewAccountName] = useState(accountName.value);
const handleAccountNameChange = () => {
accountName.setValue(newAccountName);
};
return (
<>
<h1>Account name: {accountName.value}</h1>
<label htmlFor="accountName">Account name</label>
<input
value={newAccountName}
onChange={e => setNewAccountName(e.target.value)}
id="accountName"
/>
<button
type="button"
onClick={handleAccountNameChange}>
Update
</button>
</>
);
}
How can I test that accountName has actually updated?
If we don't need the default value for the ImportAccounts provider then we make the test pass easily. ImportAccountsProvider manages the state of the accountName within itself. In that provider, we are passing the accountName state of type ImportAccountsState to all our children through the context provider.
Now coming to your problem,
const { getByText, getByLabelText, getByRole } = render(
<ImportAccountsContext.Provider value={{ accountName: { value: 'abc', setValue: jest.fn() }}}>
<ImportAccounts />
</ImportAccountsContext.Provider>,
);
Here, the value: 'abc' is not a state value, it's simply a string constant 'abc' which will never be going to change. This is something that we should note. We must pass the state value to the context provider if we want to share the value with the children which is not going to be constant in the entire react lifecycle.
import { render, screen } from '#testing-library/react';
import userEvent from '#testing-library/user-event';
test('should update the context with existing provider', () => {
render(
<ImportAccountsProvider>
<ImportAccounts />
</ImportAccountsProvider>
);
// some constant hoisted to make it clean code
const accountInput = screen.getByRole('textbox', { name: /account name/i });
const accountInputValue = 'subrato patnaik';
expect(accountInput).toHaveAttribute('value', '');
// do some changes
userEvent.type(accountInput, accountInputValue);
//validate "changes"
expect(screen.getByDisplayValue(accountInputValue)).toBeTruthy();
expect(accountInput).toHaveAttribute('value', accountInputValue);
// update context
userEvent.click(screen.getByRole('button', { name: /update/i }));
// validate update
expect(screen.getByRole('heading')).toHaveTextContent(/subrato/i);
screen.debug();
});
Inside the ImportAccountsProvider we can do the fetch call and set the accountName state to the response of the fetch call.
export const ImportAccountsProvider = ({ children }: { children: React.ReactNode }) => {
const [accountName, setAccountName] = useState('');
useEffect(() => {
// do the fetch call here and update the accountName state accordingly
});
const initialState: ImportAccountsState = {
accountName: {
value: accountName,
setValue: setAccountName,
},
};
return (
<ImportAccountsContext.Provider value={initialState}>
{children}
</ImportAccountsContext.Provider>
);
};
export const useImportAccountsContext = () => {
return useContext<ImportAccountsState>(ImportAccountsContext);
};

ReactJS Lifting State fully or Partally

I have an application where in some cases I am lifting the state to the closest ancestor. however, I came across an instance where I am lifting some of the states and leaving others locally. Is this a good practice? Does it make confusing long term?
textField Component
export const FieldInput = props => {
// local
const {
inputId,
inputLabel,
inputValue,
} = props;
return(
<>
<TextField
id={inputId}
label={inputLabel}
value={inputValue}
className={props.classes.textField}
onChange={props.handleChange}
data=""
/>
</>
)
}
export default withStyles(styles)(FieldInput);
closest ancestor with lifted state
const [textState, setTextState] = useState({
textTitle: "Please Provide Title",
textRequiermentPoc: "Please Provide POC",
textDirectingRequierment: "",
textRequiermentDescription: "",
});
const handleTitleText = (event) => {
setTextState({
textTitle: event.target.value,
})
}
const handleRequiermentPocText = (event) => {
setTextState({
textRequiermentPoc: event.target.value,
})
}
INPUT FORM
<FieldInput
inputLabel={"Requirement point of contact"}
inputId={"requiermentPOC"}
inputValue={textState.textRequiermentPoc}
handleChange={handleRequiermentPocText}>
</FieldInput>

Accessing state from another component

I am attempting to access state from another component in the correct way. From what I understand is you cannot access state directly but the state should be lifted. For my purpose I am trying to access the name of the user from my profile component in my form component with localStorage.
My Profile Component ....
import React, { Component } from 'react';
import NavigationBar from './NavigationBar'
export default class Profile extends Component {
documentData;
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.handleFormSubmit = this.handleFormSubmit.bind(this);
this.state = {
name: '',
email: '',
phone: '',
address1: '',
address2: '',
addresscitystate: '',
addresszip: ''
}
}
handleChange= (e)=> {
this.setState({[e.target.name]:e.target.value});
}
// on form submit...
handleFormSubmit(e) {
e.preventDefault()
localStorage.setItem('document',JSON.stringify(this.state));
alert ('Profile has been updated')
}
// React Life Cycle
componentDidMount() {
this.documentData = JSON.parse(localStorage.getItem('document'));
if (localStorage.getItem('document')) {
this.setState({
name: this.documentData.name,
phone: this.documentData.phone,
email: this.documentData.email,
address1: this.documentData.address1,
address2: this.documentData.address2,
addresscitystate: this.documentData.addresscitystate,
addresszip: this.documentData.addresszip,
})
} else {
this.setState({
name: '',
phone: '',
email: '',
address1: '',
address2: '',
addresscitystate: '',
addresszip: ''
})
}
}
render() {
return (
<div className="container">
<NavigationBar />
<div>
<h1 className='profile-title'> Profile </h1>
</div>
<form onSubmit={this.handleFormSubmit}>
<div className="form-group">
**<input type="text" name="name" className="form-control" value={this.state.name} onChange={this.handleChange} />
<label>Name</label>**
</div>
....
My Form Component....
import {useState, useEffect} from 'react'
const Form = () => {
//initial state
const [transaction, setTransaction] = useState({
description: '',
amount: ''
})
const [list, setList] = useState(
JSON.parse(localStorage.getItem('list')) || []
)
const [balance, setBalance] = useState('')
const [income, setIncome] = useState(
JSON.parse(localStorage.getItem('income'))
)
const [expense, setExpense] = useState(JSON.parse(localStorage.getItem('expense')))
//updates based onChange value
const updateBalance = (e) => {
setTransaction({
...transaction,
[e.target.name]:
e.target.type === 'number' ? parseInt(e.target.value) : e.target.value
})
}
//identify if transaction is income/expense
const plusMinus = () => {
transaction.amount > 0
? setIncome(income + transaction.amount)
: setExpense(expense + transaction.amount)
}
// updates balance after transaction is added
const getBalance = () => {
const amounts = list.map(i => i.amount);
const money = amounts.reduce((acc, item) => (acc += item), 0).toFixed(2);
setBalance(money)
}
useEffect(() => {
getBalance()
localStorage.setItem('list', JSON.stringify(list))
localStorage.setItem('income', JSON.stringify(income))
localStorage.setItem('expense', JSON.stringify(expense))
}, [])
//clear transaction list
const clearBudget = () => {
localStorage.clear();
alert ('Balance has been cleared, Please Refresh Page')
}
const onSubmit = e => {
e.preventDefault();
setList([transaction, ...list])
plusMinus()
setTransaction({ description: '', amount: ''})
}
return (
**<div>
<div className='totals'>
<h2 className='balance'> Hello User, Your Current Balance </h2>
<h3> ${balance} </h3>
</div>**
...
Any advise on how to start would be appreciated!
Thanks.
Since you want to make your user accessible within your whole app passing it down to your component is not advisable as it'll cause prop drilling which is not a good practise. To avoid that problem then you should consider using a built-in feature of react called ContextAPI or any other state management library like Redux of Flux. But since your app is not complex then, ContextApi would be a good choice.
When you need to access state from multiple components you should either use Context API or a State Management Library such as Redux. Please note that Redux is used mostly in bigger applications, the ContextAPI should be more than enough for your use case.

How trigger function when specific state changed

I'm looking for a way to associate a function with a change in a particular state.
I've written this code
export type MoviesAppState = {
search: string;
searchBy:string;
startDate: number;
endDate: number;
}
export class App extends React.PureComponent<{}, MoviesAppState> {
startDate = (new Date("2018-11-13")).getTime();
endDate = new Date().getTime()
state: MoviesAppState = {
search: '',
searchBy:"Select one",
startDate: this.startDate,
endDate: this.endDate,
}
onSearch = async (val: string, newPage?: number) => {
clearTimeout(this.searchDebounce);
this.searchDebounce = setTimeout(async () => {
let newMovies = await api.getMovies(val, this.state.searchBy, this.state.startDate, this.state.endDate);
this.setState({
movies: newMovies,
search: val
});
}, 300);
}
startDateChanged = (date: Date) => {
const startDateTimestamp = new Date(date).getTime()
this.setState({
startDate: startDateTimestamp,
});
}
endDateChanged = async (date: Date) => {
const endDateTimestamp = new Date(date).getTime()
this.setState({
endDate: endDateTimestamp,
});
}
onSearchBy = (searchByCriterion:string)=>{
this.setState({
searchBy: searchByCriterion
});
}
render() {
return (<main>
<h1>Movies</h1>
<div>
<header>
<input type="search" placeholder="Search..." onChange={(e) => this.onSearch(e.target.value)} />
</header>
<SearchByCriterion
searchByHandler={this.onSearchBy}
initialCriterion={this.state.searchBy}
/>
</div>
<div>
<span>Start date: </span><MyDatePicker initialDate={this.startDate} dateChangeHandler={this.startDateChanged} ></MyDatePicker>
</div>
</main>)
}
}
export default App;
Basically what I want is that onSearch method will fire when one of the things happens- startDateChanged or endDateChanged and not only when the search input changed
I thought about calling it within the methods and I get what I want but I feel it's not the best practice.
componentDidUpdate is what you are after: https://reactjs.org/docs/react-component.html#componentdidupdate
componentDidUpdate(_, prevState) {
const {startDate, endDate, searchBy} = this.state
if(startDate !== prevState.startDate || endDate !== prevState.endDate || searchBy !== prevState.searchBy){
// trigger onSearch
}
}
You can achieve this with a componentDidUpdate lifecycle hook, which runs whenever there's a props or state change and allows you to compare the change before taking an action. See docs
It should look something like this
componentDidUpdate(_prevProps, prevState) {
// Typical usage (don't forget to compare props):
if (this.state.startDate !== prevState.startDate ||
this.state.startDate !== prevState.startDate) {
this.search();
}
}

Lose state in React Js

I have an application with 2 inputs; name and password. Also, I have a save button that should change the state, using the values from the inputs, in the parent component.
But now, if I insert just one value in one input, I lose the state in the parent component. For example, if I type just name, and click save button, in the parent component I lose the password value, but I don't know why.
How to avoid this using my code?
import React, { useEffect } from "react";
import "./styles.css";
const Test = ({ user, setUser }) => {
const [u, setU] = React.useState("");
const [p, setP] = React.useState("");
function name(e) {
const a = e.target.value;
setU(a);
}
function password(e) {
const a = e.target.value;
setP(a);
}
function save() {
console.log(u);
setUser({ ...user, name: u, password: p });
}
return (
<div>
<input onChange={name} />
<input onChange={password} />
<button onClick={save}>save</button>
</div>
);
};
export default function App() {
const [user, setUser] = React.useState({
name: "",
password: ""
});
useEffect(() => {
setUser({ name: "john", password: "Doe" });
}, []);
return (
<div className="App">
<p>{user.name}</p>
<p>{user.password}</p>
<Test user={user} setUser={setUser} />
</div>
);
}
code link: https://stackblitz.com/edit/react-ytowzg
This should fix your issue:
function save() {
console.log(u);
if(u === '') {
setUser({ ...user, password: p });
} else if (p === '') {
setUser({ ...user, name: u });
} else {
setUser({ ...user, name: u, password: p });
}
}
So, now the state is conditionally updated based on the values of the input fields. The issue with your code is that you're always overwriting the state regardless of the input values.
i have a better proposition,instead of using a separate state variable for name ,password and percentage use a single state variable object
Test.js
import React, { useState } from "react";
import { InputNumber } from "antd";
import "antd/dist/antd.css";
const Test = ({ user, setUser }) => {
const [state, setState] = useState({
name: "",
password: "",
percentage: ""
});
function onChange(e, name) {
setState({
...state,
...(name === undefined
? { [e.target.name]: e.target.value }
: { [name]: e })
});
console.log(state);
}
function save() {
setUser({
...user,
...(state.name !== "" && { name: state.name }),
...(state.password !== "" && { password: state.password }),
...(state.percentage !== "" && { percentage: state.percentage })
});
}
return (
<div>
<input name='name' onChange={onChange} />
<input name='password' onChange={onChange} />
<InputNumber
defaultValue={100}
min={0}
max={100}
formatter={value => `${value}%`}
parser={value => value.replace("%", "")}
onChange={e => onChange(e, "percentage")}
/>
<button onClick={save}>save</button>
</div>
);
};
export default Test;
Updated CodeSandbox here

Resources