I have a list of objects ("Albums" in my case) fetched from the database. I need to edit these objects.
In the editing component in the useEffect hook I fire up the action for getting the needed album using it's ID. This action works. However in the same useEffect I am trying to fetch the changed by before fired action redux state. And now I face the problem - all I am fetching is the previos state.
How can I implement in the useEffect fetching of current redux state?
I've seen similar questions here, however none of the answers were helpfull for my use case.
I am using redux-thunk.
Editing component. The problem appears in setFormData - it's fetching previous state from the reducer, not the current one. It seems that it fires before the state gets changed by the getAlbumById:
//imports
const EditAlbum = ({
album: { album, loading},
createAlbum,
getAlbumById,
history,
match
}) => {
const [formData, setFormData] = useState({
albumID: null,
albumName: ''
});
useEffect(() => {
getAlbumById(match.params.id);
setFormData({
albumID: loading || !album.albumID ? '' : album.albumID,
albumName: loading || !album.albumName ? '' : album.albumName
});
}, [getAlbumById, loading]);
const { albumName, albumID } = formData;
const onChange = e =>
setFormData({ ...formData, [e.target.name]: e.target.value });
const onSubmit = e => {
e.preventDefault();
createAlbum(formData, history, true);
};
return ( //code );
};
EditAlbum.propTypes = {
createAlbum: PropTypes.func.isRequired,
getAlbumById: PropTypes.func.isRequired,
album: PropTypes.object.isRequired
};
const mapStateToProps = state => ({
album: state.album
});
export default connect(
mapStateToProps,
{ createAlbum, getAlbumById }
)(withRouter(EditAlbum));
Action:
export const getAlbumById = albumID => async dispatch => {
try {
const res = await axios.get(`/api/album/${albumID}`);
dispatch({
type: GET_ALBUM,
payload: res.data
});
} catch (err) {
dispatch({
type: ALBUMS_ERROR,
payload: { msg: err.response.statusText, status: err.response.status }
});
}
};
reducer
const initialState = {
album: null,
albums: [],
loading: true,
error: {}
};
const album = (state = initialState, action) => {
const { type, payload } = action;
switch (type) {
case GET_ALBUM:
return {
...state,
album: payload,
loading: false
};
case ALBUMS_ERROR:
return {
...state,
error: payload,
loading: false
};
default:
return state;
}
};
Will be grateful for any help/ideas
You should split up your effects in 2, one to load album when album id changes from route:
const [formData, setFormData] = useState({
albumID: match.params.id,
albumName: '',
});
const { albumName, albumID } = formData;
// Only get album by id when id changed
useEffect(() => {
getAlbumById(albumID);
}, [albumID, getAlbumById]);
And one when data has arrived to set the formData state:
// Custom hook to check if component is mounted
// This needs to be imported in your component
// https://github.com/jmlweb/isMounted
const useIsMounted = () => {
const isMounted = useRef(false);
useEffect(() => {
isMounted.current = true;
return () => (isMounted.current = false);
}, []);
return isMounted;
};
// In your component check if it's mounted
// ...because you cannot set state on unmounted component
const isMounted = useIsMounted();
useEffect(() => {
// Only if loading is false and still mounted
if (loading === false && isMounted.current) {
const { albumID, albumName } = album;
setFormData({
albumID,
albumName,
});
}
}, [album, isMounted, loading]);
Your action should set loading to true when it starts getting an album:
export const getAlbumById = albumID => async dispatch => {
try {
// Here you should dispatch an action that would
// set loading to true
// dispatch({type:'LOAD_ALBUM'})
const res = await axios.get(`/api/album/${albumID}`);
dispatch({
type: GET_ALBUM,
payload: res.data
});
} catch (err) {
dispatch({
type: ALBUMS_ERROR,
payload: { msg: err.response.statusText, status: err.response.status }
});
}
};
Update detecting why useEffect is called when it should not:
Could you update the question with the output of this?
//only get album by id when id changed
useEffect(() => {
console.log('In the get data effect');
getAlbumById(albumID);
return () => {
console.log('Clean up get data effect');
if (albumID !== pref.current.albumID) {
console.log(
'XXXX album ID changed:',
pref.current.albumID,
albumID
);
}
if (getAlbumById !== pref.current.getAlbumById) {
console.log(
'XXX getAlbumById changed',
pref.current.getAlbumById,
getAlbumById
);
}
};
}, [albumID, getAlbumById]);
Related
How to update the state just after dispatch?
State should be updated but is not.
What should be changed? Even when we will use then in our code -> even then we will not receive updated state, only when we will take value from the like .then((value) => { value.data }), but I want to take data from the state
Slice Code:
const authSlice = createSlice({
name: 'auth',
initialState: {
user: {},
},
reducers: {},
extraReducers: (builder) => {
builder
.addCase(activateUser.fulfilled, (state, action) => {
state.user = action.payload.userData
})
},
})
export const activateUser = createAsyncThunk('auth/activate', async (data) => {
try {
const userData = await authService.activateAccount(data)
return { userData: userData.data.data }
} catch (error) {
const message =
(error.response && error.response.data && error.response.data.message) ||
error.message ||
error.toString()
return message
}
})
const { reducer } = authSlice
export default reducer
Component:
function ActivateAccount() {
const { user } = useSelector((state) => state.auth)
const [code, setCode] = useState('')
const dispatch = useDispatch()
const activateUserAccount = () => {
const data = {
code: code?.value,
userId: user?._id,
email: user?.email || email?.value,
}
dispatch(activateUser(data))
console.log('Why here value is not updated yet?', user)
if (!user.activated) {
setCode({
...code,
isNotActivated: true,
})
return
}
return navigate('/on-board')
}
}
Why in the console log value is not yet updated?
What should be changed?
Any ideas?
Even though it's Redux it still needs to work within the confines of the React component lifecycle. In other words, the state needs to be updated and subscribers notified and React to rerender with the updated state. You are currently logging the user state closed over in function scope of the current render cycle. There's no possible way to log what the state will be on any subsequent render cycle.
You can chain from the asynchronous action though, and check any resolved values.
function ActivateAccount() {
const { user } = useSelector((state) => state.auth);
const [code, setCode] = useState('');
const dispatch = useDispatch();
const activateUserAccount = () => {
const data = {
code: code?.value,
userId: user?._id,
email: user?.email || email?.value,
}
dispatch(activateUser(data))
.unwrap()
.then(user => {
console.log(user);
if (!user.activated) {
setCode(code => ({
...code,
isNotActivated: true,
}));
return;
}
return navigate('/on-board');
});
}
...
}
I need help. I don't understand why my dispatch action doesn't work. I've redux store currency list and current currency.
My reducer:
export const currencyReducer = (
state: typeState = initialState,
action: TypeActionCurrency
): typeState => {
switch (action.type) {
case types.CURRENCY_FILL_LIST:
return { ...state, list: action.payload }
case types.CURRENCY_SET_CURRENT:
return {
...state,
current:
state.list.find(currency => currency._id === action.payload) ||
({} as ICurrency),
}
default:
return state
}
}
My actions:
export const setCurrencyList = (currencies: ICurrency[]) => ({
type: types.CURRENCY_FILL_LIST,
payload: currencies,
})
export const setCurrentCurrency = (_id: string) => ({
type: types.CURRENCY_SET_CURRENT,
payload: _id,
})
My useEffect:
useEffect(() => {
if (!list.length) {
const fetchCurrencies = async () => {
try {
const data = await $apiClient<ICurrency[]>({ url: '/currencies' })
dispatch(setCurrencyList(data))
if (!current._id) dispatch(setCurrentCurrency(data[0]._id))
} catch (error) {
console.log(error)
}
}
fetchCurrencies()
}
}, [])
I want make request when load page and write currency list to Redux store, if we don't have current currency we write default currency from data.
There is one more strange thing, my redux extension shows that the state has changed, but when I receive it via the log or useSelector, it is empty
enter image description here
Thanks!
I am not 100% sure but it should work.
const [loader, setLoader] = useState(false);
const list = useSelector(state => state.list)
useEffect(() => {
if (!list.length) {
const fetchCurrencies = async () => {
try {
setLoader(true)
const data = await $apiClient<ICurrency[]>({ url: '/currencies' })
dispatch(setCurrencyList(data))
if (!current._id) dispatch(setCurrentCurrency(data[0]._id))
} catch (error) {
console.log(error)
} finally {
setLoader(false)
}
}
fetchCurrencies()
}
}, [])
useEffect(() => {
console.log(list);
}, [loader])
I'm trying to update an object property previously declared in a useState hook for form values and save it in localstorage. Everything goes well, but localstorage is saving date property empty all the time, I know that it must be because of asynchrony but I can't find the solution. This is my code. I'm newbie with React hooks. Lot of thanks!
const [formValues,setformValues] = useState(
{
userName:'',
tweetText:'',
date:''
}
)
const getlocalValue = () => {
const localValue = JSON.parse(localStorage.getItem('tweetList'));
if(localValue !== null){
return localValue
} else {
return []
}
}
const [tweetList,setTweetList] = useState(getlocalValue());
const handleInput = (inputName,inputValue) => {
setformValues((prevFormValues) => {
return {
...prevFormValues,
[inputName]:inputValue
}
})
}
const handleForm = () => {
const {userName,tweetText} = formValues;
if(!userName || !tweetText) {
console.log('your tweet is empty');
} else {
setformValues(prevFormValues => {
return {
...prevFormValues,
date:getCurrentDate() //this is not updating in local
}
})
setTweetList(prevTweets => ([...prevTweets, formValues]));
toggleHidden(!isOpen)
}
}
console.log(formValues) //but you can see changes outside the function
useEffect(() => {
localStorage.setItem('tweetList', JSON.stringify(tweetList));
}, [tweetList]);
In this case the issue is because the handleForm that was called still only has access to the formValues state at the time it was called, rather than the new state. So, the easiest way to handle this is to just update the formValues, setFormValues, and then setTweetList based on the local copy of the updated formValues.
const handleForm = () => {
const {userName,tweetText} = formValues;
if(!userName || !tweetText) {
console.log('your tweet is empty');
} else {
const updatedFormValues = {...formValues,date:getCurrentDate()};
setformValues(updatedFormValues)
setTweetList(prevTweets => ([...prevTweets, updatedFormValues]));
toggleHidden(!isOpen)
}
}
Since there's issues with concurrency here: i.e. you can't guarantee an update to the state of both formValues and tweetList with the latest data. Another option is useReducer instead of the two separate state variables because they are related properties and you'd be able to update them based off of each other more easily.
As an example of making more complicated updates with reducers, I added a 'FINALIZE_TWEET' action that will perform both parts of the action at once.
const Component = () => {
const [{ formValues, tweetList }, dispatch] = useReducer(
reducer,
undefined,
getInitState
);
const handleInput = (inputName, inputValue) => {
dispatch({ type: 'SET_FORM_VALUE', payload: { inputName, inputValue } });
};
const handleForm = () => {
const { userName, tweetText } = formValues;
if (!userName || !tweetText) {
console.log('your tweet is empty');
} else {
dispatch({ type: 'SET_FORM_DATE' });
dispatch({ type: 'PUSH_TO_LIST' });
// OR
// dispatch({type: 'FINALIZE_TWEET'})
toggleHidden(!isOpen);
}
};
console.log(formValues); //but you can see changes outside the function
useEffect(() => {
localStorage.setItem('tweetList', JSON.stringify(tweetList));
}, [tweetList]);
return <div></div>;
};
const getlocalValue = () => {
const localValue = JSON.parse(localStorage.getItem('tweetList'));
if (localValue !== null) {
return localValue;
} else {
return [];
}
};
function getInitState() {
const initialState = {
formValues: {
userName: '',
tweetText: '',
date: '',
},
tweetList: getlocalValue(),
};
}
function reducer(state, action) {
switch (action.type) {
case 'SET_FORM_VALUE':
return {
...state,
formValues: {
...state.formValues,
[action.payload.inputName]: action.payload.inputValue,
},
};
case 'SET_FORM_DATE':
return {
...state,
formValues: {
...state.formValues,
date: getCurrentDate(),
},
};
case 'PUSH_TO_LIST':
return {
...state,
tweetList: [...state.tweetList, state.formValues],
};
case 'FINALIZE_TWEET': {
const newTweet = {
...state.formValues,
date: getCurrentDate(),
};
return {
...state,
formValues: newTweet,
tweetList: [...state.tweetList, newTweet],
};
}
default:
return state;
}
}
const handleFilter = (filter_type, selection) => {
dispatch({ type: filter_type, option: selection.option })
filterData()
}
I have a useReducer that handles the selected filter. I have to call the filterData which filters the data based on the selected filter.
However, since it is not guaranteed for the filterData to occur after the dispatch, I keep getting delayed update in the state.
I also tried
const handleFilter = (filter_type, selection) => {
dispatch({ type: filter_type, option: selection.option })
.then(filterData())
}
But this gave me an error Cannot read property 'then' of undefined.
Any help?
EDIT
useEffect(() => {
const urls = [
'http://.../v1/profiles',
];
const fetchJson = url => fetch(url, get_options).then(res => res.json());
Promise.all(urls.map(fetchJson))
.then(([profile]) => {
setFilteredTable(profile.result)
})
.catch(err => {
console.log(err)
});
}, []);
const init_filter = { TITLE: '', LOCATION: '' }
const [filter, dispatch] = useReducer((state, action) => {
switch (action.type) {
case 'TITLE':
return { ...state, TITLE: action.option }
case 'LOCATION':
return { ...state, LOCATION: action.option }
case 'RESET':
return init_filter
default:
return state
}
}, init_filter)
I have to call the filterData which filters the data based on the selected filter.
You describing a listener, you can try using a useEffect hook:
const [filter, dispatch] = useReducer(optionsReducer, initialValue);
useEffect(() => {
filterData();
}, [filter]);
const handleFilter = (filter_type, selection) => {
dispatch({ type: filter_type, option: selection.option });
};
I've read the docs here but I am having trouble getting the component to rerender after state is updated. The posts are being added, I just have to rerender the component manually to get them to show up, what am I missing?
I have this in the component:
class ListPosts extends Component {
state = {
open: false,
body: '',
id: ''
}
openPostModal = () => this.setState(() => ({
open: true,
}))
closePostModal = () => this.setState(() => ({
open: false,
}))
componentWillMount() {
const selectedCategory = this.props.selectedCategory;
this.props.fetchPosts(selectedCategory);
}
handleChange = (e, value) => {
e.preventDefault();
// console.log('handlechange!', e.target.value)
this.setState({ body: e.target.value });
};
submit = (e) => {
// e.preventDefault();
console.log(this.state.body)
const body = this.state.body;
const id = getUUID()
const category = this.props.selectedCategory;
const post = {
id,
body,
category
}
this.props.dispatch(addPost(post))
this.closePostModal()
}
Then down below I am adding the dispatch to props...
const mapStateToProps = state => ({
posts: state.postsReducer.posts,
loading: state.postsReducer.loading,
error: state.postsReducer.error,
selectedCategory: state.categoriesReducer.selectedCategory,
// selectedPost: state.postsReducer.selectedPost,
});
function mapDispatchToProps (dispatch) {
return {
fetchPosts: (selectedCategory) => dispatch(fetchPosts(selectedCategory)),
addPost: (postObj) => dispatch(addPost(postObj)),
}
}
export default withRouter(connect(
mapStateToProps,
mapDispatchToProps
)(ListPosts))
Here is the code for the reducer:
case C.ADD_POST :
const hasPost = state.some(post => post.id === action.payload.postObj.id)
console.log('caseADD_POST:', action.payload.postObj.id)
return (hasPost) ?
state :
[
...state,
post(null, action)
];