I'm having an issue when repopulating form for edit using react hooks.
Parent Component : Edit.js
const EditData = (props) => {
const { Id } = props.match.params;
const dispatch = useDispatch();
// calling redux action to get the data
useEffect(() => {
dispatch(getDataById(Id));
}, [Id]);
const data = useSelector((state) => state.data);
const initialState = {
Id: data.cardId || '',
Number: data.Number || '',
Date: data.Date,
};
//calling custom hook
const { handleChange, handleSubmit, values,errors } = useForm(
initialState,// passing initial state to custom hook
validateOnSubmit,
submit
);
// used to submit the data
function submit() {
dispatch(updateCard(values));
}
return (<DateForm
handleSubmit={handleSubmit}
handleChange={handleChange}
values={values}
/>);
};
Custom hook: useform.js
const useForm = (initialState, validateOnSubmit, callback) => {
const [values, setValues] = useState(initialState);
const [errors, setErrors] = useState({});
const [isSubmitting, setIsSubmitting] = useState(false);
const handleChange = (event) => {
const { name, value } = event.target;
setValues({
...values,
[name]: value
});
};
const handleSubmit = (event) => {
event.preventDefault();
setErrors(validateOnSubmit(values));
setIsSubmitting(true);
};
useEffect(() => {
if (Object.keys(errors).length === 0 && isSubmitting) {
callback();
}
}, [errors]);
return {
handleChange,
handleSubmit,
values,
errors
};
};
when the API call get finished the react re-render the custom but the local state of the hook is not updating.
const useForm = (initialState, validateOnSubmit, callback) => {
console.log(initialState);
on second render, here i can receive data from the API
const [values, setValues] = useState(initialState);
but values is not getting updated, values is still holding the state from the initial render
I cannot figure out why this is. I'm just started to use react hooks, please help me.
As OP stated in a comment:
the initialState variable is updated when the API call get completed,I'm passing that initialState variable to const [values, setValues] = useState(initialState); , so it should update the values variable right?. but it's not!
It is should update the state, the initial state is assigned once until the component unmounts.
See useState API, it stated in lazy initialization:
The initialState argument is the state used during the initial render. In subsequent renders, it is disregarded.
Related
i've been solving this problem without any progress for the pas 2 hours or so, here is code:
export const useFetchAll = () => {
const [searchResult, setSearchResult] = useState([]);
const [loading, setLoading] = useState(false);
const [searchItem, setSearchItem] = useState("");
const [listToDisplay, setListToDisplay] = useState([]);
// const debouncedSearch = useDebounce(searchItem, 300);
const handleChange = (e) => {
setSearchItem(e.target.value);
if (searchItem === "") {
setListToDisplay([]);
} else {
setListToDisplay(
searchResult.filter((item) => {
return item.name.toLowerCase().includes(searchItem.toLowerCase());
})
);
}
console.log(searchItem);
};
useEffect(() => {
const searchRepo = async () => {
setLoading(true);
const { data } = await axios.get("https://api.github.com/repositories");
setSearchResult(data);
setLoading(false);
};
if (searchItem) searchRepo();
}, [searchItem]);
the problem is that when i enter characters in input and set state to event.target.value it doesn't pick up last character. here is an image:
enter image description here
BTW this is a custom hook, i return the onchange function here:
const HomePage = () => {
const { searchResult, loading, searchItem, handleChange, listToDisplay } =
useFetchAll();
and then pass it as a prop to a component like so:
<Stack spacing={2}>
<Search searchItem={searchItem} handleChange={handleChange} />
</Stack>
</Container>
any help? thanks in advance.
You are handling the searchItem and searchResult state variables as if their state change was synchronous (via setSearchItem and setSearchResult) but it isn't! React state setters are asynchronous.
The useEffect callback has a dependency on the searchItem state variable. Now every time the user types something, the state will change, that change will trigger a re-rendering of the Component and after that render finishes, the side-effect (the useEffect callback) will be executed due to the Components' lifecycle.
In our case, we don't want to initiate the fetch request on the next render, but right at the moment that the user enters something on the search input field, that is when the handleChange gets triggered.
In order to make the code work as expected, we need some a more structural refactoring.
You can get rid of the useEffect and handle the flow through the handleChange method:
export const useFetchAll = () => {
const [ loading, setLoading ] = useState( false );
const [ searchItem, setSearchItem ] = useState( "" );
const [ listToDisplay, setListToDisplay ] = useState( [] );
const handleChange = async ( e ) => {
const { value } = e.target;
// Return early if the input is an empty string:
setSearchItem( value );
if ( value === "" ) {
return setListToDisplay( [] );
}
setLoading( true );
const { data } = await axios.get( "https://api.github.com/repositories" );
setLoading( false );
const valueLowercase = value.toLowerCase(); // Tiny optimization so that we don't run the toLowerCase operation on each iteration of the filter process below
setListToDisplay(
data.filter(({ name }) => name.toLowerCase().includes(valueLowercase))
);
};
return {
searchItem,
handleChange,
loading,
listToDisplay,
};
};
function used for updating state value is asynchronous that why your state variable is showing previous value and not the updated value.
I have made some change you can try running the below code .
const [searchResult, setSearchResult] = useState([]);
const [loading, setLoading] = useState(false);
const [searchItem, setSearchItem] = useState("");
const [listToDisplay, setListToDisplay] = useState([]);
// const debouncedSearch = useDebounce(searchItem, 300);
const handleChange = (e) => {
setSearchItem(e.target.value); // this sets value asyncronously
console.log("e.target.value :" + e.target.value); // event.target.value does not omitting last character
console.log("searchItem :" + searchItem); // if we check the value then it is not set. it will update asyncronously
};
const setList = async () => {
if (searchItem === "") {
setListToDisplay([]);
} else {
setListToDisplay(
searchResult.filter((item) => {
return item.name.toLowerCase().includes(searchItem.toLowerCase());
})
);
}
};
const searchRepo = async () => {
const { data } = await axios.get("https://api.github.com/repositories");
setSearchResult(data);
setLoading(false);
};
// this useeffect execute its call back when searchItem changes a
useEffect(() => {
setList(); // called here to use previous value stored in 'searchResult' and display something ( uncomment it if you want to display only updated value )
if (searchItem) searchRepo();
}, [searchItem]);
// this useeffect execute when axios set fetched data in 'searchResult'
useEffect(() => {
setList();
}, [searchResult]);
// this useeffect execute when data is updated in 'listToDisplay'
useEffect(() => {
console.log("filtered Data") // final 'listToDisplay' will be availble here
console.log(listToDisplay)
}, [listToDisplay]);
I'm a little new to react and i can't understand why my object property is undefined but when i console.log my object is appearing okay see this screenshot:
This is my custom hook useForm:
const useForm = (callback, validateRegister) => {
const [values, setValues] = useState({
name: '',
email: '',
password: '',
confirmPass: '',
});
const [errors, setErrors] = useState({});
const [isSubmitting, setIsSubmitting] = useState(false);
const handleChange = (event) => {
const { name, value } = event.target;
setValues({
...values,
[name]: value,
});
};
const handleSubmit = (event) => {
event.preventDefault();
setErrors(validateRegister(values)); // validateReister is another function that returns and object with these properties.
setIsSubmitting(true);
};
useEffect(() => {
if (Object.keys(errors).length === 0 && isSubmitting) {
callback();
}
}, [errors]);
return {
handleChange,
handleSubmit,
values,
errors,
};
};
export default useForm;
Component:
const { handleChange, handleSubmit, values, errors } = useForm(
submit,
validateRegister
);
Problem:
{errors.nameError}
Is not showing up, is not appearing on console.log either. Any idea?
I think your validateRegister(values) returns a Promise. Try changing your implementation to the below :-
const handleSubmit = (event) => {
event.preventDefault();
validateRegister(values).then(data => setErrors(data)).catch(err => console.log(err));
setIsSubmitting(true);
};
Replace setErrors(validateRegister(values)); with
validateRegister(values).then(data => setErrors(data)).catch(e => console.log(e));
I have a component I want to redirect to using react router. How can I set the state of the new component with a string that I chose on the original component? All of my redirects using react router are working and this component that is being redirected to isn't working. It is a html button when clicked should render this new components with initial data.
const Posts = (props) => {
const dispatch = useDispatch();
const getProfile = async (member) => {
console.log(member)
props.history.push('/member', { user: member});
console.log('----------- member------------')
}
const socialNetworkContract = useSelector((state) => state.socialNetworkContract)
return (
<div>
{socialNetworkContract.posts.map((p, index) => {
return <tr key={index}>
<button onClick={() => getProfile(p.publisher)}>Profile</button>
</tr>})}
</div>
)
}
export default Posts;
This is the component I am trying to redirect to on click.
const Member = (props)=> {
const [user, setUser] = useState({});
const { state } = this.props.history.location;
const socialNetworkContract = useSelector((state) => state.socialNetworkContract)
useEffect(async()=>{
try {
await setUser(state.user)
console.log(user)
console.log(user)
const p = await incidentsInstance.usersProfile(state.user, { from: accounts[0] });
const a = await snInstance.getUsersPosts(state.user, { from: accounts[0] });
} catch (e) {
console.error(e)
}
}, [])
I get the following error in the console.
TypeError: Cannot read property 'props' of undefined
Member
src/components/profiles/member.js:16
13 | const [posts, setPosts] = useState([]);
14 | const [snInstance, setsnInstance] = useState({});
15 | const [accounts, setsAccounts] = useState({});
> 16 | const { state } = this.props.history.location;
If you need to send some route state then the push method takes an object.
const getProfile = (member) => {
console.log(member)
props.history.push({
pathname: '/member',
state: {
user: member,
},
});
console.log('----------- member------------')
}
Additionally, Member is a functional component, so there is no this, just use the props object.
The route state is on the location prop, not the history object.
const Member = (props)=> {
const [user, setUser] = useState({});
const { state } = props.location;
// access state.user
Also additionally, useEffect callbacks can't be async as these imperatively return a Promise, interpreted as an effect cleanup function. You should declare an internal async function to invoke. On top of this, the setuser function isn't async so it can't be awaited on.
The following is what I think should be the effects for populating the user state and issuing side-effects:
// update user state when route state updates
useEffect(() => {
if (state && state.user) {
setUser(state.user);
}
}, [state]);
// run effect when user state updates
useEffect(() => {
const doEffects = async () => {
try {
const p = await incidentsInstance.usersProfile(state.user, { from: accounts[0] });
const a = await snInstance.getUsersPosts(state.user, { from: accounts[0] });
} catch (e) {
console.error(e)
}
}
doEffects();
}, [user]);
I am attempting to query my Firebase backend through a redux-thunk action, however, when I do so in my initial render using useEffect(), I end up with this 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.
My action simply returns a Firebase query snapshot which I then received in my reducer. I use a hook to dispatch my action:
export const useAnswersState = () => {
return {
answers: useSelector(state => selectAnswers(state)),
isAnswersLoading: useSelector(state => selectAnswersLoading(state))
}
}
export const useAnswersDispatch = () => {
const dispatch = useDispatch()
return {
// getAnswersData is a redux-thunk action that returns a firebase snapshot
setAnswers: questionID => dispatch(getAnswersData(questionID))
}
}
and the following selectors to get the data I need from my snapshot and redux states:
export const selectAnswers = state => {
const { snapshot } = state.root.answers
if (snapshot === null) return []
let answers = []
snapshot.docs.map(doc => {
answers.push(doc.data())
})
return answers
}
export const selectAnswersLoading = state => {
return state.root.answers.queryLoading || state.root.answers.snapshot === null
}
In my actual component, I then attempt to first query my backend by dispatching my action, and then I try reading the resulting data once the data is loaded as follows:
const params = useParams() // params.id is just an ID string
const { setAnswers, isAnswersLoading } = useAnswersDispatch()
const { answers } = useAnswersState()
useEffect(() => {
setAnswers(params.id)
}, [])
if (!isAnswersLoading)) console.log(answers)
So to clarify, I am using my useAnswersDispatch to dispatch a redux-thunk action which returns a firebase data snapshot. I then use my useAnswersState hook to access the data once it is loaded. I am trying to dispatch my query in the useEffect of my actual view component, and then display the data using my state hook.
However, when I attempt to print the value of answers, I get the error from above. I would greatly appreciate any help and would be happy to provide any more information if that would help at all, however, I have tested my reducer and the action itself, both of which are working as expected so I believe the problem lies in the files described above.
Try refactoring your action creator so that dispatch is called within the effect. You need to make dispatch dependent on the effect firing.
See related
const setAnswers = (params.id) => {
const dispatch = useDispatch();
useEffect(() => {
dispatch(useAnswersDispatch(params.id));
}, [])
}
AssuminggetAnswersData is a selector, the effect will trigger dispatch to your application state, and when you get your response back, your selector getAnswersData selects the fields you want.
I'm not sure where params.id is coming from, but your component is dependent on it to determine an answer from the application state.
After you trigger your dispatch, only the application state is updated, but not the component state. Setting a variable with useDispatch, you have variable reference to the dispatch function of your redux store in the lifecycle of the component.
To answer your question, if you want it to handle multiple dispatches, add params.id and dispatch into the dependencies array in your effect.
// Handle null or undefined param.id
const answers = (param.id) => getAnswersData(param.id);
const dispatch = useDispatch();
useEffect(() => {
if(params.id)
dispatch(useAnswersDispatch(params.id));
}, [params.id, dispatch]);
console.log(answers);
As commented; I think your actual code that infinite loops has a dependency on setAnswers. In your question you forgot to add this dependency but code below shows how you can prevent setAnswers to change and cause an infinite loop:
const GOT_DATA = 'GOT_DATA';
const reducer = (state, action) => {
const { type, payload } = action;
console.log('in reducer', type, payload);
if (type === GOT_DATA) {
return { ...state, data: payload };
}
return state;
};
//I guess you imported this and this won't change so
// useCallback doesn't see it as a dependency
const getAnswersData = id => ({
type: GOT_DATA,
payload: id,
});
const useAnswersDispatch = dispatch => {
// const dispatch = useDispatch(); //react-redux useDispatch will never change
//never re create setAnswers because it causes the
// effect to run again since it is a dependency of your effect
const setAnswers = React.useCallback(
questionID => dispatch(getAnswersData(questionID)),
//your linter may complain because it doesn't know
// useDispatch always returns the same dispatch function
[dispatch]
);
return {
setAnswers,
};
};
const Data = ({ id }) => {
//fake redux
const [state, dispatch] = React.useReducer(reducer, {
data: [],
});
const { setAnswers } = useAnswersDispatch(dispatch);
React.useEffect(() => {
setAnswers(id);
}, [id, setAnswers]);
return <pre>{JSON.stringify(state.data)}</pre>;
};
const App = () => {
const [id, setId] = React.useState(88);
return (
<div>
<button onClick={() => setId(id => id + 1)}>
increase id
</button>
<Data id={id} />
</div>
);
};
ReactDOM.render(<App />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>
Here is your original code causing infinite loop because setAnswers keeps changing.
const GOT_DATA = 'GOT_DATA';
const reducer = (state, action) => {
const { type, payload } = action;
console.log('in reducer', type, payload);
if (type === GOT_DATA) {
return { ...state, data: payload };
}
return state;
};
//I guess you imported this and this won't change so
// useCallback doesn't see it as a dependency
const getAnswersData = id => ({
type: GOT_DATA,
payload: id,
});
const useAnswersDispatch = dispatch => {
return {
//re creating setAnswers, calling this will cause
// state.data to be set causing Data to re render
// and because setAnser has changed it'll cause the
// effect to re run and setAnswers to be called ...
setAnswers: questionID =>
dispatch(getAnswersData(questionID)),
};
};
let timesRedered = 0;
const Data = ({ id }) => {
//fake redux
const [state, dispatch] = React.useReducer(reducer, {
data: [],
});
//securit to prevent infinite loop
timesRedered++;
if (timesRedered > 20) {
throw new Error('infinite loop');
}
const { setAnswers } = useAnswersDispatch(dispatch);
React.useEffect(() => {
setAnswers(id);
}, [id, setAnswers]);
return <pre>{JSON.stringify(state.data)}</pre>;
};
const App = () => {
const [id, setId] = React.useState(88);
return (
<div>
<button onClick={() => setId(id => id + 1)}>
increase id
</button>
<Data id={id} />
</div>
);
};
ReactDOM.render(<App />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>
You just need to add params.id as a dependency.
Don't dispatch inside the function which you are calling inside useEffect but call another useEffect to dispatch
const [yourData, setyourData] = useState({});
useEffect(() => {
GetYourData();
}, []);
useEffect(() => {
if (yourData) {
//call dispatch action
dispatch(setDatatoRedux(yourData));
}
}, [yourData]);
const GetYourData= () => {
fetch('https://reactnative.dev/movies.json')
.then((response) => response.json())
.then((json) => {
if (result?.success == 1) {
setyourData(result);
}
})
.catch((error) => {
console.error(error);
});
};
I am learning react-hooks, I created a series of state variables using useState, when trying to debug and see its value I find that React Developer Tool does not show the name assigned to the state variable but the text State, this is inconvenient as it is not possible to identify from the beginning which variable is the one that is tried to debug
Update 1
This is the current source code
import React, { useState, useEffect, Fragment } from "react";
function Main() {
const [data, setData] = useState({ hits: [] });
const [query, setQuery] = useState("redux");
const [url, setUrl] = useState(
"https://hn.algolia.com/api/v1/search?query=redux"
);
const [isLoading, setIsLoading] = useState(false);
const [isError, setIsError] = useState(false);
useEffect(() => {
const fetchData = async () => {
setIsError(false);
setIsLoading(true);
try {
const response = await fetch(url);
const json = await response.json();
setData(json);
} catch (e) {
setIsError(true);
}
setIsLoading(false);
};
fetchData();
}, [url]);
return (
<Fragment>
<form
onSubmit={event => {
setUrl(`http://hn.algolia.com/api/v1/search?query=${query}`);
event.preventDefault();
}}
>
<input value={query} onChange={event => setQuery(event.target.value)} />
<button type="submit">Search</button>
</form>
{isError && <div>Something went wrong ...</div>}
{isLoading ? (
<div>Loading...</div>
) : (
<ul>
{data.hits.map(item => (
<li key={item.objectID}>
<a href={item.url}>{item.title}</a>
</li>
))}
</ul>
)}
</Fragment>
);
}
export default Main;
I am getting this in React Developer tool
Updated 2
I am using Firefox 68
Is it possible that React Developer Tool shows the name of state variables created using useState?
See this issue:
https://github.com/facebook/react-devtools/issues/1215#issuecomment-479937560
That's the normal behavior for the dev tool when using hooks.
I asked the library author about it, cause I also would like it to show my state variable names. And that's what he said:
#cbdeveloper I haven't thought of a good way for DevTools to be able to display the variable name like you're asking. DevTools doesn't have a way to read your function's private variables, and changing the API to support passing in a display name would increase the size of component code. It also just doesn't seem necessary to me, more like a nice to have.
Anyway, this umbrella issue is not the best place to have discussions like this. If you feel strongly about it, I suggest opening a new issue.
From "normal" useState hook implementation:
const [users, setUser] = useState([]);
const [profile, setProfile] = useState([]);
const [repo, setRepo] = useState([]);
const [loading, setLoading] = useState(false);
const [alert, setAlert] = useState(false);
You can "convert" it in:
const [state, setState] = useState({ users: [], profile: [], repo: [], loading: false, alert: false });
And you will get the following result:
And to set the state you can use the rest operator(source 1 / source 2) and the state you want to set:
// ...state => unchanged states
// alert => state we want to change
setState(state => ({ ...state, alert: true }));
to use it as a prop:
const {
users,
profile,
repo,
loading,
alert
} = state;
<SomeComponent loading={loading} alert={alert} />
You see the React docs here and search for: Should I use one or many state variables?
You can use useDebugValue to display a custom label in your own hook:
const format = ({ label, value }) =>
label + ': ' + (typeof value === 'object' ? JSON.stringify(value) : value);
const useNamedState = (label, initialState) => {
const states = useState(initialState);
useDebugValue({ label, value: states[0] }, format);
return states;
};
Usage
Before
const [name, setName] = useState('bob');
const [age, setAge] = useState(11);
const [address, setAddress] = useState('London, United Kingdom');
const [isVirgin, setIsVirgin] = useState(false);
const [isMale, setIsMale] = useState(true);
const [hobbies, setHobbies] = useState(['gaming', 'hiking', 'cooking']);
const [friendList, setFriendList] = useState(['bob']);
After
const [name, setName] = useNamedState('name', 'bob');
const [age, setAge] = useNamedState('age', 11);
const [address, setAddress] = useNamedState('address', 'London, United Kingdom');
const [isVirgin, setIsVirgin] = useNamedState('isVirgin', false);
const [isMale, setIsMale] = useNamedState('isMale', true);
const [hobbies, setHobbies] = useNamedState('hobbies', ['gaming', 'hiking', 'cooking']);
const [friendList, setFriendList] = useNamedState('friendList', ['bob']);
Hmm, I'm not sure why it doesn't show the names, but if I'm trying to create a series of state variables, I sometimes just initialize a state and setState variable as an empty object. I'm not sure if this is the best way, but maybe in your dev tools, it'll show the state attribute name when you expand it.
import React from "react";
export default function App(props) {
// Initializing the state
const [state, setState] = React.useState({});
// Changing the state
setState({ ...state, text1: "hello world", text2: "foobar" });
return (
<div>
{state.text1}
<br />
{state.text2}
</div>
);
}