Setting Initial State to response from API with REDUX - javascript

I'm having trouble getting my head around setting the api response to the initial state object.
I've tried using the useAsyncThunk as stated in the documentation, however the project I'm working on only lets me use redux-thunk.
The goal is to download the usernames from https://jsonplaceholder.typicode.com/users and use that to create an autofill input field that suggests usernames.

I don't understand why you want to initialise your usernames already as initialState. That's not how the redux pattern works. Normally, you initialise your store with reasonable empty states:
const initialState = {
loading: false,
usernames: [],
}
const reducer = (state = initialState, action) => {
switch (action.type) {
case LOADING_USERNAMES:
return {
...state,
loading: true,
};
case USERNAMES_LOADED:
return {
...state,
loading: false,
usernames: action.payload.usernames,
};
default:
return state;
}
}
export default reducer;
And then you set up your thunks and plain actions:
export const LOADING_USERNAMES = '[User] loading usernames';
export const USERNAMES_LOADED = '[User] usernames loaded';
export const loadingUsernames = () => ({
type: LOADING_USERNAMES,
});
export const usernamesLoaded = (usernames) => ({
type: USERNAMES_LOADED,
payload: {
usernames,
}
});
const getUsernameList = () => (dispatch) => {
dispatch(loadingUsernames()); // for the loading state
return fetch('https://jsonplaceholder.typicode.com/users')
.then(response => dispatch(usernamesLoaded(response.json())))
And then in your useEffect hook wherever you want to initialize this list, you call the thunk:
dispatch(getUsernameList());
You can decide not to show anything as long as loading is true and no list is present, but in my opinion a good UI lets the user know that something is happening. Maybe I didn't understand your question properly, but this at least would be my approach.
You state, that you want this for an autofill, so maybe don't show the form until your data is ready, or alternatively show a spinner.

Related

useSelector not updating after dispatching an action React Redux

I know there are already a couple of questions similar to this, but none of the solutions provided are working for me. I am dispatching an action in my React Native component, but when I try to access the state afterwards with useSelector, it is still just the previous state, not the updated one.
My component:
const userSettings = useSelector((state: any) => state.user.settings);
const loadUserSettings = async () => {
await dispatch(userActions.fetchUserSettings());
}
const checkUserEnabled = async () => {
await loadUserSettings();
// The old state shows here rather than the updated one
console.log(userSettings.enabled)
}
and this is my reducer:
const initialState = {
settings = UserSettings,
};
export default (state = initialState, action) => {
switch (action.type) {
case SET_USER_SETTINGS:
return {
...state,
settings: action.settings,
};
default:
return state;
}
};
userSettings is a stale closure, as Nicolas suggested you cannot dispatch an action and expect the state value to have changed immediately because a new state is created after every action and your current function is still using the old value of the state. When the component re renders it will be re rendered with the new state value.

How to nest Redux Toolkit reducers for a single property

I'm migrating a codebase from vanilla Redux to Redux Toolkit. I'm trying to find a good way to nest reducers created with createReducer just for a single property.
Let's say I have a setup like the following contrived example with a user reducer and a friends reducer nested under it. The user can change their name, which only affects itself, and also add and remove their friends, which affects itself and its friends array property that is managed by the friends reducer.
const CHANGE_NAME = "CHANGE_NAME";
const ADD_FRIEND = "ADD_FRIEND";
const REMOVE_ALL_FRIENDS = "REMOVE_ALL_FRIENDS";
const initialState = {
username: "",
email: "",
lastActivity: 0,
friends: [],
};
const user = (state = initialState, action) => {
switch (action.type) {
case CHANGE_NAME: {
const { newName, time } = action.payload;
return {
...state,
name: newName,
lastActivity: time,
};
}
case ADD_FRIEND:
case REMOVE_ALL_FRIENDS: {
const { time } = action.payload;
return {
...state,
friends: friends(state.friends, action),
lastActivity: time,
};
}
default: {
return {
...state,
friends: friends(state.friends, action),
};
}
}
};
const friends = (state = initialState.friends, action) => {
switch (action.type) {
case ADD_FRIEND: {
const { newFriend } = action.payload;
return [...state, newFriend];
}
case REMOVE_ALL_FRIENDS: {
return [];
}
default: {
return state;
}
}
};
To note:
friends is necessarily correlated with user, so I had decided to nest it within its state slice.
user manually calls the friends reducer to calculate the friends slice of state with possibly overlapping action types, and only for that one friends property.
I am now trying to refactor this with Redux Toolkit createReducers. My first attempt was the following:
import { createReducer } from "#reduxjs/toolkit";
const CHANGE_NAME = "CHANGE_NAME";
const ADD_FRIEND = "ADD_FRIEND";
const REMOVE_ALL_FRIENDS = "REMOVE_ALL_FRIENDS";
const initialState = {
username: "",
email: "",
lastActivity: 0,
friends: [],
};
const user = createReducer(initialState, (builder) => {
builder
.addCase(CHANGE_NAME, (state, action) => {
const { newName, time } = action.payload;
state.name = newName;
state.lastActivity = time;
})
.addMatcher((action) => action.type === ADD_FRIEND || action.type === REMOVE_ALL_FRIENDS),
(state, action) => {
const { time } = action.payload;
state.lastActivity = time;
state.friends = friends(state.friends, action);
};
});
const friends = createReducer(initialState, (builder) => {
builder
.addCase(ADD_FRIEND, (state, action) => {
const { newFriend } = action.payload;
state.push(newFriend);
})
.addCase(REMOVE_ALL_FRIENDS, () => []);
});
To note:
The last matcher for the user reducer is the main focus here.
The friends reducer now has two ways of modifying the state: "modifying" the state by pushing to it with .push, and returning a new empty state by directly returning [].
In my intuition this would work as it appears to be the same logic. However, this only works for the ADD_FRIEND action, and does nothing or emits an error about simultaneously modifying state and returning a new state for the REMOVE_ALL_FRIENDS action type.
This seems to be because the state being modified turns it to an ImmerJS Proxy object in the user reducer, but when it is passed to the friends reducer and it returns a state object directly instead of modifying it causing RTK to throw an error as it says you must only modify or return state, but not both. In the handler for ADD_FRIEND this is not an issue as it always modifies the state, the same as all the handlers in user.
As a hacky workaround I have manually checked whether the friends reducer returns a Proxy or a new state directly, and if it returns a new state then it sets it in the user reducer, but I am sure there is a better way:
import { createReducer, current } from "#reduxjs/toolkit";
const user = createReducer(initialState, (builder) => {
builder
.addMatcher((action) => action.type === ADD_FRIEND || action.type === REMOVE_ALL_FRIENDS),
(state, action) => {
const { time } = action.payload;
state.lastActivity = time;
const result = friends(state.friends, action);
let output;
// If state has been returned directly this will error and we can set the state manually,
// Else this will not error because a Proxy has been returned, and thus the state has been
// set already by the sub-reducer.
try {
output = current(result);
} catch (error) {
output = result;
}
if (output) {
state.progress = output;
}
};
});
My question is then how can I fix this so that I don't have to manually check the return type and can easily nest RTK reducers within each other, whether it be by restructuring my reducers or fixing the code logic?
Ideally I would still like to keep the friends reducer nested under the user reducer as that is how a lot of "vanilla" Redux code structures their state logic with many different reducers handling many different pieces of state, instead of them all being nested at the root-level with a single combineReducers call, but if there is a better and cleaner solution given I am fine with that too.
Thanks for any help, and sorry for the long post - just wanted to be as detailed as possible as other solutions online didn't seem to address this exact problem.
The issue was that my original user reducer code was reducer was returning a new state object by spreading the state and setting the friends property in that object spread. This produced an error from ImmerJS as it was returning a new value from the user reducer and was also modifying it in the friends reducer at the same time.
My posted code worked (with some modifications thanks to Linda), but to fix my original code (and I had not posted the version that produced errors - apologies) I had to change the following:
.addMatcher(
(action) =>
action.type === ADD_FRIEND || action.type === REMOVE_ALL_FRIENDS,
(state, action) => ({
...state,
lastActivity: action.payload.time,
friends: friends(state.friends, action)
})
)
to:
.addMatcher(
(action) =>
action.type === ADD_FRIEND || action.type === REMOVE_ALL_FRIENDS,
(state, action) => {
const { time } = action.payload;
state.lastActivity = time;
state.friends = friends(state.friends, action);
}
)
Thanks for the help, everyone.
In this particular case it's easy to handle the friends property in the user reducer: state.friends.push(newFriend) or state.friends = []. But there shouldn't be any issue with keeping it separate.
I did notice a few issues when trying to run your code:
Using the initialState for the whole user as the initial state of friends, instead of initialState.friends or []
Unmatched parentheses in addMatcher around the action.type check
Assigning to state.name instead of state.username
After fixing those I was not able to reproduce your issue. I am able to add and remove friends successfully.
This could actually be a bug in Redux Toolkit. Could you please file an issue with a reproduction CodeSandbox over at out github issue tracker?

REDUX: store variables accessed via this.props are outdated but store.getState() works

Well hello there!
I'm having some issues - that I never had before - by accessing store variables through mapStateToProps. Namely, they never change and always have their default value I setup in the store BEFORE changing them in any way. If I call them by store.getState().reducer.x my code works!
Here's my store:
export const initialState = {
isKeyManagementWindowOpen: false
};
const rootReducer = combineReducers({
some: someReducer,
settings: settingsComponentReducer
)};
const store = createStore(rootReducer, compose(applyMiddleware(thunk), window.__REDUX_DEVTOOLS_EXTENSION__ ? window.__REDUX_DEVTOOLS_EXTENSION__() : variable => variable));
export default store;
settingsComponentActions.js
export const TOGGLE_KEY_MANAGEMENT_WINDOW = 'TOGGLE_KEY_MANAGEMENT_WINDOW';
export const toggleKeyManagementWindow = isKeyManagementWindowOpen => {
return { type: TOGGLE_KEY_MANAGEMENT_WINDOW, isKeyManagementWindowOpen};
}
settingsComponentReducer.js
export const settingsComponentReducer = (state = initialState, action) => {
console.log(action);
switch (action.type) {
case Actions.TOGGLE_KEY_MANAGEMENT_WINDOW:
return Object.assign({}, state, {
isKeyManagementWindowOpen: action.isKeyManagementWindowOpen
});
default: return state;
}
};
One thing that may be causing issues is that I am calling this.props in my websocket's subscribe method.
Key.js
connectToWebsocket = ip => {
const stompClient = Stomp.client(`url/receivekey`);
stompClient.heartbeat.outgoing = 0;
stompClient.heartbeat.incoming = 0;
stompClient.debug = () => null;
stompClient.connect({ name: ip }, frame => this.stompSuccessCallBack(frame, stompClient), err => this.stompFailureCallBack(err, ip));
}
stompSuccessCallBack = (frame, stompClient) => {
stompClient.subscribe(KEY_READER_NODE, keyData => {
if (!this.props.isKeyManagementWindowOpen) {
this.loginWithKey(keyData.body);
} else {
this.addToKeyList(keyData.body);
}
});
}
Even though I set isKeyManagementWindowOpen beforehand to true it still resolves to false. If I swap !this.props.isKeyManagementWindowOpen with !store.getState().settings.isKeyManagementWindowOpen the code works and it goes into this.addToKeyList(keyData.body).
So, if I swap those but LEAVE every store call in this.addToKeyList as this.props. then those are all default valued too, which doesn't make sense. It only works if I swap every this.props. line with store.getState()....
const mapStateToProps = state => ({
...
...
isKeyManagementWindowOpen: state.settings.isKeyManagementWindowOpen,
});
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(Key));
As of now, my code works but I'd like to call the props as this.props... and not via store.getState().... Any idea why this could happen?
Thanks!
Seems like you're using deep state
Object assign only makes shallow copies of objects. So let's try to eliminate the easiest possible cause.
export const settingsComponentReducer = (state = initialState, action) => {
const newState = JSON.parse(JSON.stringify(state));
Then use newState instead of state below.
This will make a deep copy of your state and will always be a new object forcing your app to see it as a new prop and re-render correctly.
Why not use something like this, as you shouldn't directly mutate the overall state of the app, only update it if an action is triggered but spread the original state in prior to updating.
export const settingsComponentReducer = (state = initialState, action) => {
console.log(action);
switch (action.type) {
case Actions.TOGGLE_KEY_MANAGEMENT_WINDOW:
return {
...state,
isKeyManagementWindowOpen: action.isKeyManagementWindowOpen
});
default:
return state;
}
};
The problem is that React cannot have updated any value in this.props by the time the next line of code has executed.
This is not actually a Redux-specific problem. In any React component, triggering a state change on a line will still result in the same props and state values on the next line, because the current function is still executing and React has not re-rendered yet.

Infinite loop during useEffect and Reducer

I don't know why but I have infinite loop when fetching data in Redux operations.
I have an app with Redux and ReactJS.
This is my React component
const CustomersTable = (props) => {
useEffect( () => {
props.getAllCustomers()
}, []);
return <Table ...props.customers />
}
const mapStateToProps = (state) => ({
customers: state.customers,
})
const mapDispatchToProps = dispatch => ({
getAllCustomers: () => dispatch(getAllCustomers()),
})
export default connect(
mapStateToProps, mapDispatchToProps
)(CustomersTable);
This is getAllInvoices()
const fetchCustomers = async() => {
/**
* I fetch only documents with flag delete==false
*/
const snapshot = await firestore.collection("customers").where('deleted', '==', false).get()
let data = []
snapshot.forEach(doc => {
let d = doc.data();
d.id_db = doc.id
//...other
data.push(d)
})
return data
}
export const getAllCustomers = () =>
async (dispatch) => {
const customers = await fetchCustomers()
// I reset state becouse I wont duplicate inovices in tables
dispatch(actions.reset())
customers.map(customer => dispatch(
actions.fetch(customer)
))
}
And reducers
const customerReducer = (state = INITIAL_STATE, action) => {
switch (action.type) {
case types.FETCH_CUSTOMERS:
return {
...state, list: [...state.list, action.item]
}
case types.RESET_CUSTOMERS:
return {
...state, list: []
}
default:
return state
}
}
I expect that reducers RESET_CUSTOMERS and then FETCH_CUSTOMERS done job. But it still working in loop reset->customers.
I thought that is still rendered the component in useEffect but I think that hook is writing good.
I tested other reducers which are copy-pase reducers from Customers and they work well.
EDIT 1
#godsenal, thanks for your reply:
actions.js:
import types from './types'
const fetch = item => ({
type: types.FETCH_CUSTOMERS, item
})
const reset = item => ({
type: types.RESET_CUSTOMERS, item
})
export default {
fetch,
reset
}
As regards <Table /> it is AntDesign component (https://ant.design/components/table/). Without that, it looks the same.
EDIT 2
It is incredible. I copied all files from modules (customers) and paste into contracts directory. Then I changed all variables, functions, etc from customer to contract. Now it working (only contracts), but customers infinite loop. Maybe something disturbs in outside a structure.
EDIT 3
I found in app.js that in mapStateToProps I added customers to props. After remove (because I don't need it in root component) it began works fine. I suspect that fetch method in <CustomerTable /> affect the <App /> component and it render in a loop. I discovered that component isn't still updated in a loop, but its mounts and unmounts in a loop.
But still, I don't understand one thing. In <App />, I still have in mapStateToProps dispatching invoice from a store (the same case as customers) and in this case, everything works fine.

Async Action with Redux

I have a React component that's connected to Redux store. I'm fetching resources(posts) in the componentWillMount life-cycle method.
componentWillMount() {
this.props.fetchPosts();
}
The component will subscribe to Redux store and getting isFetching and posts from the store.
const mapStateToProps = (state) => {
return {
posts: getAllPosts(state),
isFetching: getIsFetchingPosts(state),
}
}
I'd like to show a spinner when it's still fetching, so in the render method I'd like to do this.
render() {
if (this.props.isFetching) {
return <Spinner />
}
return this.props.posts.map(post => <PostItem key={post.id}{...post}/>)
}
BUT if I console.log isFetching in the render method, first it shows false and thentrue and then finally false.
Ideally when this container renders for the first time isFetching state is already set to true and showing the spinner. What changes do I need to make to make that happen?
Here is code for the action creator and reducers
/*** Action Creator ***/
export const fetchPosts = () => (dispatch) => {
dispatch({
type: REQUEST_POSTS,
});
return axios({
method: 'get',
url: `${API_URL}/posts`,
})
.then(({data}) => {
dispatch({
type: RECEIVE_POSTS,
payload: data.posts,
})
})
.catch((response) => {
// some error handling.
});
}
/*** Reducers ***/
const initialState = {
isFetching: false,
allIds: [],
byId: {},
};
const isFetching = (state = initialState.isFetcthing, action) => {
switch (action.type) {
case REQUEST_POSTS:
return true;
case RECEIVE_POSTS:
return false;
default:
return state;
}
}
const allIds = (state = initialState.allIds, action) => {
switch (action.type) {
case RECEIVE_POSTS:
return action.payload.map(post => post.id);
default:
return state;
}
}
const byId = (state = initialState.byId, action) => {
switch (action.type) {
case RECEIVE_POSTS:
return action.payload.reduce((nextState, post) => {
nextState[post.id] = post;
return nextState;
}, {...state});
default:
return state;
}
}
const posts = combineReducers({
isFetching,
allIds,
byId,
});
export default posts;
/*** Selectors in 'posts.js' file ***/
export const getAllPosts = (state) => {
const { allId, byId } = state;
return allIds.map(id => byId[id]);
}
/*** rootReducer file ***/
import posts, * as fromPosts from './posts';
const rootReducer = combineReducers({
posts,
})
export default rootReducer;
export const getAllPosts = (state) => {
return fromPosts.getAllPosts(state.posts);
};
Thank you in advance!
The plain answer is that this is the expected behaviour, given your implementation. You're mapping the isFetching state to a prop. Here's what's happening:
The initial value of isFetching in the state tree is false, so the isFetching prop value is false, so it renders as false.
You dispatch an action which changes isFetching to true in the state tree. This new state is mapped to a new isFetching prop value of true, which causes a re-render, where it renders as true.
You (asynchronously) dispatch another action which changes isFetching back to false in the state tree. Same as in (2), this causes a re-render, with isFetching as false.
The plain solution if you want simply a render of true, false*, with the current implementation, is to set isFetching to true in your reducer's initial state.
Whether or not this implementation makes sense on a design level for this component is a broader question, that there's not enough context to answer here :-)
*update for completeness I should say I don't know if the render() function would be called twice, with isFetching resolved as true,false or three times with true,true,false in this case. I suspect that react-redux may optimise the rendering of the component such that no re-render occurs if the mapped isFetching prop changes from true->true, but don't know this for sure - would be grateful and interested if you could let me know what your logging outputs?
In any case, at the DOM level certainly only two renders would occur, due to the standard react virtual-DOM diffing optimisation, so in effect the result is the same either way

Categories

Resources