The challenge I came across is using global store slice, namely 'genres', which is an array of objects, in a local state to manipulate check/uncheck of the checkboxes. The problem occurs when I'm trying to use props.genres in the initial state. Looks like I'm getting an empty array from props.genres when the local state is initialized.
const Filters = (props) => {
const { genres, getSelected, loadGenres, getGenres, clearFilters } = props
const [isChecked, setIsChecked] = useState(() =>
genres.map(genre => (
{id: genre.id, value: genre.name, checked: false}
))
)
const optionsSortBy = [
{name: 'Popularity descending', value: 'popularity.desc'},
{name: 'Popularity ascending', value: 'popularity.asc'},
{name: 'Rating descending', value: 'vote_average.desc'},
{name: 'Rating ascending', value: 'vote_average.asc'},
]
const d = new Date()
let currentYear = d.getFullYear()
let optionsReleaseDate = R.range(1990, currentYear + 1).map(year => (
{name: year + '', value: year}
))
useEffect(() => {
const url = `${C.API_ENDPOINT}genre/movie/list`
loadGenres(url, C.OPTIONS)
}, [])
const handleCheckbox = (e) => {
let target = e.target
getGenres(target)
}
const handleSelect = (e) => {
let target = e.target
let action = isNaN(target.value) ? 'SORT_BY' : 'RELEASE_DATE'
getSelected(action, target)
}
const handleSubmitBtn = (e) => {
e.preventDefault()
clearFilters()
}
return (
<form className={classes.FiltersBox}>
<Submit submited={handleSubmitBtn} />
<Select name="Sort By:" options={optionsSortBy} changed={handleSelect} />
<Select name="Release date:" options={optionsReleaseDate} changed={handleSelect} />
<Genres genres={isChecked} changed={handleCheckbox} />
</form>
)
}
const mapStateToProps = (state) => {
return {
genres: state.fetch.genres,
}
}
const mapDispatchToProps = (dispatch) => {
return {
loadGenres: (url, options) => dispatch(A.getApiData(url, options)),
getGenres: (targetItem) => dispatch({
type: 'CHECK_GENRES',
payload: targetItem
}),
getSelected: (actionType, targetItem) => dispatch({
type: actionType,
payload: targetItem,
}),
clearFilters: () => dispatch({type: 'CLEAR_FILTERS'})
}
}
export default connect(mapStateToProps, mapDispatchToProps)(Filters);
import * as R from 'ramda';
import fetchJSON from '../utils/api.js';
export const getApiData = (url, options) => async (dispatch) => {
const response = await fetchJSON(url, options)
const data = response.body
const dataHas = R.has(R.__, data)
let actionType = dataHas('genres') ? 'FETCH_GENRES' : 'FETCH_MOVIES'
dispatch({
type: actionType,
payload: data
})
}
export const fetchReducer = (state = initialState, action) => {
const { payload } = action
if (action.type === 'FETCH_GENRES') {
return {
...state,
isLoading: false,
genres: [...payload.genres]
}
}
if (action.type === 'FETCH_MOVIES') {
return {
...state,
isLoading: false,
movies: [...payload.results]
}
}
return state
}
What you are trying to do of setting initial value for state from props, is possible but isn't react best practice. Consider initial your data as empty array and through useEffect manipulate state
// didn't understand if its array or bool
const [isChecked, setIsChecked] = useState([])
useEffect(()=>genres&& { setIsChecked(... perform action...)
} ,[genres])
You approach is almost correct.
I am not sure how the state should look like, when you have fetched your data.
I can see in the mapStateToProps is trying to access a value which is not defined at the beginning. If state.fetch is undefined you can not access genres.
Attempt 1:
You can solve it by using lodash.get https://lodash.com/docs/#get
It will catch up for the undefined problem.
Attempt 2:
You can defined an initial state where your values are defined with mock data.
const initialState = {fetch: {genres: []}}
and use it your reducer
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');
});
}
...
}
Anybody has experience in AsyncStorage in React Native? It returns wired values something like this.
"_U": 0,
"_V": 1,
"_X": null,
"_W": {}
And here is Context, useReducer hook code.
const [localState, localDispatch] = useReducer(
local,
localInitialState,
async () => {
await AsyncStorage.removeItem(‘local’);
const storedLocalData = await AsyncStorage.getItem(‘local’);
console.log(‘LOCAL: ’, storedLocalData);
storedLocalData ? console.log(‘LOCAL-YES’) : console.log(‘LOCAL-NO’);
return storedLocalData ? JSON.parse(storedLocalData) : localInitialState;
},
);
const [themeState, themeDispatch] = useReducer(
themeReducer,
themeInitialState,
async () => {
await AsyncStorage.removeItem(‘theme’);
const storedThemeData = await AsyncStorage.getItem(‘theme’);
console.log(‘THEME: ’, storedThemeData);
storedThemeData ? console.log(‘THEME-YES’) : console.log(‘THEME-NO’);
return storedThemeData ? JSON.parse(storedThemeData) : themeInitialState;
},
);
Local state works well but theme sate which copied from local does not work...
And this is Console state.
Local state already stored in Asyncstorage. but Theme state returns null.. 😦
with the same code..
the State should be works like local state. not the theme state.
I hope any advise, Thanks.
Unfortunately there's no possibility for useReducer to have a function that returns a Promise as initializer for now! (which I think it's necessary for the next updates of React)
but here's my solution for now: (written in typescript)
import React from "react";
import { CommonActionTypes } from "context/common/CommonActions";
import useStorage from "./useStorage";
/**
* --- IMPORTANT ----
* if you're using this wrapper, your reducer must handle the ReplaceStateAction
* **Also** your state needs to have a property named `isPersistedDataReady` with `false` as default value
*/
export function usePersistedReducer<State, Action>(
reducer: (state: State, action: Action) => State,
initialState: State,
storageKey: string,
): [State, React.Dispatch<Action>] {
const { value, setValue, isReady } = useStorage<State>(storageKey, initialState);
const reducerLocalStorage = React.useCallback(
(state: State, action: Action): State => {
const newState = reducer(state, action);
setValue(newState);
return newState;
},
[value],
);
const [store, dispatch] = React.useReducer(reducerLocalStorage, value);
React.useEffect(() => {
isReady &&
// #ts-ignore here we need an extension of union type for Action
dispatch({
type: CommonActionTypes.ReplaceState,
state: { ...value, isPersistedDataReady: true },
});
}, [isReady]);
return [store, dispatch];
}
then in your views isPersistedDataReady value.
here's also the implementation of the hook useStorage
import AsyncStorage from "#react-native-async-storage/async-storage";
const useStorage = <T>(key: string, defaultValue: T) => {
type State = { value: T; isReady: boolean };
const [state, setState] = React.useState<State>({
value: defaultValue,
isReady: false,
});
React.useEffect(() => {
get()
.then((value) => {
setState({ value, isReady: true });
})
.catch(() => {
setState({ value: defaultValue, isReady: true });
});
}, []);
React.useEffect(() => {
state.value && state.isReady && save(state.value);
}, [state.value]);
const setValue = (value: T) => {
setState({ value, isReady: true });
};
const save = (value: T): Promise<void> => {
if (value) {
try {
const savingValue = JSON.stringify(value);
return AsyncStorage.setItem(key, savingValue);
} catch (er) {
return Promise.reject(er);
}
} else {
return Promise.reject(Error("No value provided"));
}
};
const get = (): Promise<T> => {
return AsyncStorage.getItem(key, () => defaultValue).then((value) => {
if (value === null) {
throw Error(`no value exsits for ${key} key in the storage`);
}
return JSON.parse(value);
});
};
const remove = (): Promise<void> => {
return AsyncStorage.removeItem(key);
};
return { ...state, setValue, clear: remove };
};
export default useStorage;
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])
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 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]);