console always returns "true" meaning except first button click
import React, {useState} from 'react'
function App() {
let [itemState, setLike] = React.useState( [
{ id:1, likeIt:false, }
])
function addToWish(id){
setLike( itemState.map(item=> {
if(item.id === id){
item.likeIt = !item.likeIt
}
return itemState
}))
}
console.log(itemState)
return(<button onClick={()=> addToWish()}></button>);}
The correct way to update the state in your case would be to use a functional update and also in the callback passed to map you should return the item.
function addToWish(id) {
setLike((prevState) =>
prevState.map((item) =>
item.id === id ? { ...item, likeIt: !item.likeIt } : item
)
);
}
Related
here is what the static method looks like, I want to change the class component to a functional component but functional components don't support it. I am still learning, any advice will be appreciated
static getDerivedStateFromProps(nextProps, prevState) {
if (nextProps.security.validToken) {
setTimeout(() => {
const product_type = localStorage.getItem("types");
if (product_type === "imp") {
nextProps.history.push("/imported");
} else if (product_type === "branded") {
nextProps.history.push("/brands");
} else if (product_type === "imp,branded" || product_type === "branded,imp") {
nextProps.history.push("/imported-brands");
} else if (product_type === "local") {
nextProps.history.push("/local");
}
}, 1000);
}
Would be easier if you could share the whole component, or at least how/where you plan to manage your functional component's state. As said on this anwser:
This has a number of ways that it can be done, but the best way is situational.
Let's see how we could create a ProductsRouter functional component.
import React from 'react'
import { useHistory } from 'react-router'
export const ProductsRouter = ({ security }) => {
// our page state
const [productType, setProductType] = React.useState('')
// history object instance
const history = useHistory()
// our routing effect
React.useEffect(() => {
// if not authenticated, do nothing
if (!security.validToken) return
// update our state
setProductType(localStorage.getItem('types'))
// timeout function
const timer = setTimeout(() => {
if (productType === 'imp') {
history.push('/imported')
} else if (productType === 'branded') {
history.push('/brands')
} else if (productType === 'imp,branded' || productType === 'branded,imp') {
history.push('/imported-brands')
} else if (productType === 'local') {
history.push('/local')
}
}, 1000)
return () => clearTimeout(timer)
}, [])
return <pre>{JSON.stringify({ history }, null, 2)}</pre>
}
We could use a map to keep our code tidy and flexible. If there's no particular reason to have a 1000ms timeout, we could also call push instantly, as long as we have a validToken.
const typesMap = new Map([
['imp', '/imported'],
['branded', '/brands'],
['imp,branded', '/imported-brands'],
['branded,imp', '/imported-brands'],
['local', '/local'],
])
export const ProductsRouter = ({ security }) => {
const history = useHistory()
React.useEffect(() => {
if (!security.validToken) return
history.push(typesMap.get(localStorage.getItem('types')))
return () => {}
}, [security.validToken])
return <pre>{JSON.stringify({ history }, null, 2)}</pre>
}
Hope that helps! Cheers
I have a FilterContext provider and a hook useFilter in filtersContext.js:
import React, { useState, useEffect, useCallback } from 'react'
const FiltersContext = React.createContext({})
function FiltersProvider({ children }) {
const [filters, setFilters] = useState({})
return (
<FiltersContext.Provider
value={{
filters,
setFilters,
}}
>
{children}
</FiltersContext.Provider>
)
}
function useFilters(setPage) {
const context = React.useContext(FiltersContext)
if (context === undefined) {
throw new Error('useFilters must be used within a FiltersProvider')
}
const {
filters,
setFilters
} = context
useEffect(() => {
return () => {
console.log('reset the filters to an empty object')
setFilters({})
}
}, [setFilters])
{... do some additional stuff with filters if needed... not relevant }
return {
...context,
filtersForQuery: {
...filters
}
}
}
export { FiltersProvider, useFilters }
The App.js utilises the Provider as:
import React from 'react'
import { FiltersProvider } from '../filtersContext'
const App = React.memo(
({ children }) => {
...
...
return (
...
<FiltersProvider>
<RightSide flex={1} flexDirection={'column'}>
<Box flex={1}>
{children}
</Box>
</RightSide>
</FiltersProvider>
...
)
}
)
export default App
that is said, everything within FiltersProvider becomes the context of filters.
Now comes the problem description: I have selected on one page (Page1) the filter, but when I have to switch to another page (Page2), I need to flush the filters. This is done in the useFilters hook in the unmount using return in useEffect.
The problem is in the new page (Page2), during the first render I'm still getting the old values of filters, and than the GraphQL request is sent just after that. Afterwards the unmount of the hook happens and the second render of the new page (Page2) happens with set to empty object filters.
If anyone had a similar problem and had solved it?
first Page1.js:
const Page1 = () => {
....
const { filtersForQuery } = useFilters()
const { loading, error, data } = useQuery(GET_THINGS, {
variables: {
filter: filtersForQuery
}
})
....
}
second Page2.js:
const Page2 = () => {
....
const { filtersForQuery } = useFilters()
console.log('page 2')
const { loading, error, data } = useQuery(GET_THINGS, {
variables: {
filter: filtersForQuery
}
})
....
}
Printout after clicking from page 1 to page 2:
1. filters {isActive: {id: true}}
2. filters {isActive: {id: true}}
3. page 2
4. reset the filters to an empty object
5. 2 reset the filters to an empty object
6. filters {}
7. page 2
As I mentioned in the comment it might be related to the cache which I would assume you are using something like GraphQL Apollo. It has an option to disable cache for queries:
fetchPolicy: "no-cache",
By the way you can also do that reset process within the Page Two component if you want to:
const PageTwo = () => {
const context = useFilters();
useEffect(() => {
context.setFilters({});
}, [context]);
For those in struggle:
import React, { useState, useEffect, useCallback, **useRef** } from 'react'
const FiltersContext = React.createContext({})
function FiltersProvider({ children }) {
const [filters, setFilters] = useState({})
return (
<FiltersContext.Provider
value={{
filters,
setFilters,
}}
>
{children}
</FiltersContext.Provider>
)
}
function useFilters(setPage) {
const isInitialRender = useRef(true)
const context = React.useContext(FiltersContext)
if (context === undefined) {
throw new Error('useFilters must be used within a FiltersProvider')
}
const {
filters,
setFilters
} = context
useEffect(() => {
**isInitialRender.current = false**
return () => {
console.log('reset the filters to an empty object')
setFilters({})
}
}, [setFilters])
{... do some additional stuff with filters if needed... not relevant }
return {
...context,
filtersForQuery: { // <---- here the filtersForQuery is another variable than just filters. This I have omitted in the question. I will modify it.
**...(isInitialRender.current ? {} : filters)**
}
}
}
export { FiltersProvider, useFilters }
What is done here: set the useRef bool varialbe and set it to true, as long as it is true return always an empty object, as the first render happens and/or the setFilters function updates, set the isInitialRender.current to false. such that we return updated (not empty) filter object with the hook.
I've been coding a flexible button component where I pass some values by param (text, class, size, icon, and such) and when I do a condition (if it's a link or a button) before returning the HTML, it asks me for a key prop.
Why is it asking for a key prop if it's not a list. I'm not even going through any array to return the value.
This is the React button component code:
import React from "react";
import "./button.css";
import { Icon } from '../Icon/icon';
const buttonTypes = [
"button",
"a"
];
const buttonClasses = [
"app-button",
"app-button-filled",
"app-button-outlined",
"app-button-icon"
];
const buttonSizes = [
"app-button-large",
"app-button-icon-large"
];
export const Button = ({
buttonIcon = {
name: '',
style: '',
position: ''
},
buttonText,
buttonType,
buttonTarget,
buttonHref,
buttonOnClick,
buttonClass,
buttonSize
}) => {
const checkClasses = () => {
if(buttonClasses.includes(buttonClass)){
return buttonClasses[0]+" "+buttonClass;
} else {
return buttonClasses[0];
}
}
const checkSizes = () => {
if(buttonSizes.includes(buttonSize)){
return buttonSize;
} else {
return '';
}
}
const checkTypes = () => {
if(buttonTypes.includes(buttonType)){
return buttonType;
} else {
return buttonTypes[0];
}
}
const insertContent = () => {
let content = [],
iconTag = <Icon iconName={buttonIcon.name} iconStyle={buttonIcon.style} iconClass="app-button-svg" />;
if(buttonClass === "app-button-icon"){
content.push(iconTag);
} else {
if(buttonText){ content.push(<span className="app-button-text">{buttonText}</span>); }
if(buttonIcon){
if(buttonIcon.position === "left"){
content.unshift(iconTag);
} else if(buttonIcon.position === "right" || buttonIcon.position !== "left") {
content.push(iconTag);
}
}
}
return content;
}
if(checkTypes() === "button"){
return (<button className={`${checkClasses()} ${checkSizes()}`} onClick={buttonOnClick}>{insertContent()}</button>);
} else if(checkTypes() === "a"){
return (<a className={`${checkClasses()} ${checkSizes()}`} href={buttonHref} target={buttonTarget} >{insertContent()}</a>);
}
}
Warning code:
Warning: Each child in a list should have a unique "key" prop | button.js:84
The condition for this trigger is passing an array in the list of children.
You are doing this near the end:
return (<button className={`${checkClasses()} ${checkSizes()}`} onClick={buttonOnClick}>{insertContent()}</button>);
...
return (<a className={`${checkClasses()} ${checkSizes()}`} href={buttonHref} target={buttonTarget} >{insertContent()}</a>);
Your children here are the result of insertContent(), which returns an array. Because it returns an array, it triggers the warning because no keys are specified.
To solve this, you need to change the function insertContent to include keys on the elements it puts into the content array
useEffect doesn't trigger on second change, but triggers twice on launch (React hooks and apollo-graphql hooks).
In console.logs I described when the changes are triggered and when not.
I don't have any more clue to add.
Here's my page (Next.js pages)
import React, { useEffect, useState } from 'react'
import Calendar from '../components/Calendar';
import { useHallsQuery } from '../generated/graphql';
const schedule = () => {
const { data, loading, error, refetch:refetchSessions } = useHallsQuery();
const [sessions, setSessions] = useState([] as any);
const [owners, setOwners] = useState([] as any);
useEffect(() => {
console.log('On launch trigger twice, on first change trigger once, on second doesn't trigger and later trigger correctly, but it's one change delay');
if(loading === false && data){
const colors: any = [
'#FF3333',
'#3333FF',
'#FFFF33',
'#33FF33',
'#33FFFF',
'#9933FF',
'#FF9933',
'#FF33FF',
'#FF3399',
'#A0A0A0'
];
let pushSessions:any = [];
let owners:any = [];
data?.halls?.map(({sessions, ...o}, index) =>{
owners.push({id:o.id,
text:o.name,
color: colors[index%10]});
sessions.map((session:any) => {
pushSessions.push({...session,
ownerId: o.id});
})
})
setSessions(pushSessions);
setOwners(owners);
}
}, [loading, data])
if (loading) return <div>Loading...</div>
if (error) return <div>Error: {error.message}</div>
return (
<div>
<Calendar sessions={sessions} owners={owners} refetchSessions={refetchSessions} />
</div>
)
}
export default schedule
and my component part where I get props and trigger refetchSessions.
const Calendar = (props: any) => {
let { sessions, owners, refetchSessions } = props;
const [moveSession] = useMoveSessionMutation();
...
const commitChanges = ({ added, changed, deleted }: any) => {
if (added) {
//
}
if (changed) {
console.log('trigger on all changes! Correct')
const id = Object.keys(changed)[0];
moveSession({variables:{
input: {
id: parseInt(id),
...changed[id]
}
}, refetchQueries: refetchSessions
})
}
if (deleted !== undefined) {
//
}
};
return (
// Some material-ui components and #devexpress/dx-react-scheduler-material-ui components in which commitChanges function is handled
)
export default Calendar;
Hook was generated with graphql-codegen(generated/graphql.tsx):
export function useHallsQuery(baseOptions?: Apollo.QueryHookOptions<HallsQuery, HallsQueryVariables>) {
return Apollo.useQuery<HallsQuery, HallsQueryVariables>(HallsDocument, baseOptions);
}
export function useHallsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<HallsQuery, HallsQueryVariables>) {
return Apollo.useLazyQuery<HallsQuery, HallsQueryVariables>(HallsDocument, baseOptions);
}
export type HallsQueryHookResult = ReturnType<typeof useHallsQuery>;
and here's the schema(graphql/hall.graphql):
query Halls {
halls {
id
name
sessions{
id
title
startDate
endDate
}
}
}
I have this application that has a deprecated lifecycle method:
componentWillReceiveProps(nextProps) {
if (this.state.displayErrors) {
this._validate(nextProps);
}
}
Currently, I have used the UNSAFE_ flag:
UNSAFE_componentWillReceiveProps(nextProps) {
if (this.state.displayErrors) {
this._validate(nextProps);
}
}
I have left it like this because when I attempted to refactor it to:
componentDidUpdate(prevProps, prevState) {
if (this.state.displayErrors) {
this._validate(prevProps, prevState);
}
}
It created another bug that gave me this error:
Invariant Violation: 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.
It starts to happen when a user clicks on the PAY NOW button that kicks off the _handlePayButtonPress which also checks for validation of credit card information like so:
UNSAFE_componentWillReceiveProps(nextProps) {
if (this.state.displayErrors) {
this._validate(nextProps);
}
}
_validate = props => {
const { cardExpireDate, cardNumber, csv, nameOnCard } = props;
const validationErrors = {
date: cardExpireDate.trim() ? "" : "Is Required",
cardNumber: cardNumber.trim() ? "" : "Is Required",
csv: csv.trim() ? "" : "Is Required",
name: nameOnCard.trim() ? "" : "Is Required"
};
if (validationErrors.csv === "" && csv.trim().length < 3) {
validationErrors.csv = "Must be 3 or 4 digits";
}
const fullErrors = {
...validationErrors,
...this.props.validationErrors
};
const isValid = Object.keys(fullErrors).reduce((acc, curr) => {
if (fullErrors[curr]) {
return false;
}
return acc;
}, true);
if (isValid) {
this.setState({ validationErrors: {} });
//register
} else {
this.setState({ validationErrors, displayErrors: true });
}
return isValid;
};
_handlePayButtonPress = () => {
const isValid = this._validate(this.props);
if (isValid) {
console.log("Good to go!");
}
if (isValid) {
this.setState({ processingPayment: true });
this.props
.submitEventRegistration()
.then(() => {
this.setState({ processingPayment: false });
//eslint-disable-next-line
this.props.navigation.navigate("PaymentConfirmation");
})
.catch(({ title, message }) => {
Alert.alert(
title,
message,
[
{
text: "OK",
onPress: () => {
this.setState({ processingPayment: false });
}
}
],
{
cancelable: false
}
);
});
} else {
alert("Please correct the errors before continuing.");
}
};
Unfortunately, I do not have enough experience with Hooks and I have failed at refactoring that deprecated lifecycle method to one that would not create trouble like it was doing with the above error. Any suggestions at a better CDU or any other ideas?
You need another check so you don't get in an infinite loop (every time you call setState you will rerender -> component did update -> update again ...)
You could do something like this:
componentDidUpdate(prevProps, prevState) {
if (this.state.displayErrors && prevProps !== this.props) {
this._validate(prevProps, prevState);
}
}
Also I think that you need to call your validate with new props and state:
this._validate(this.props, this.state);
Hope this helps.
componentDidUpdate shouldn't replace componentWillRecieveProps for this reason. The replacement React gave us was getDerivedStateFromProps which you can read about here https://medium.com/#baphemot/understanding-react-react-16-3-component-life-cycle-23129bc7a705. However, getDerivedStateFromProps is a static function so you'll have to replace all the setState lines in _validate and return an object instead.
This is how you work with prevState and hooks.
Working sample Codesandbox.io
import React, { useState, useEffect } from "react";
import "./styles.css";
const ZeroToTen = ({ value }) => {
const [myValue, setMyValue] = useState(0);
const [isValid, setIsValid] = useState(true);
const validate = value => {
var result = value >= 0 && value <= 10;
setIsValid(result);
return result;
};
useEffect(() => {
setMyValue(prevState => (validate(value) ? value : prevState));
}, [value]);
return (
<>
<span>{myValue}</span>
<p>
{isValid
? `${value} Is Valid`
: `${value} is Invalid, last good value is ${myValue}`}
</p>
</>
);
};
export default function App() {
const [value, setValue] = useState(0);
return (
<div className="App">
<button value={value} onClick={e => setValue(prevState => prevState - 1)}>
Decrement
</button>
<button value={value} onClick={e => setValue(prevState => prevState + 1)}>
Increment
</button>
<p>Current Value: {value}</p>
<ZeroToTen value={value} />
</div>
);
}
We have two components, one to increase/decrease a number and the other one to hold a number between 0 and 10.
The first component is using prevState to increment the value like this:
onClick={e => setValue(prevState => prevState - 1)}
It can increment/decrement as much as you want.
The second component is receiving its input from the first component, but it will validate the value every time it is updated and will allow values between 0 and 10.
useEffect(() => {
setMyValue(prevState => (validate(value) ? value : prevState));
}, [value]);
In this case I'm using two hooks to trigger the validation every time 'value' is updated.
If you are not familiar with hooks yet, this may be confusing, but the main idea is that with hooks you need to focus on a single property/state to validate changes.