What types of things would cause Immer only supports setting array indices and the 'length' property' from the code below? This FoodLogState type is a class. I've done something very similar with no issue. I notice I am not updating even the array from state yet. Only the status that is a string.
import { createAsyncThunk, createSlice } from "#reduxjs/toolkit";
import { FoodLogState, getFoodState } from "../AccountAPI";
export interface FoodLogs {
foodLogs: Array<FoodLogState>;
status: "idle" | "loading" | "failed";
}
const initialState: FoodLogs = {
foodLogs: null,
status: "idle",
};
export const getFoodLogsAsync = createAsyncThunk(
"foodsLogged/getFoodsLogged",
async (uid: string, { rejectWithValue }) => {
try {
const response = await getFoodState(uid).catch((error) => {
return rejectWithValue(error.message);
});
return response;
} catch (error) {
return rejectWithValue(error.message);
}
}
);
const foodsLogSlice = createSlice({
name: "foodsLogged",
initialState,
reducers: {},
extraReducers: (builder) => {
builder
.addCase(getFoodLogsAsync.fulfilled, (state, action) => {
//state.foodLogs = action.payload;
state.status = "idle";
})
.addCase(getFoodLogsAsync.rejected, (state, action) => {
state.status = "failed";
})
.addCase(getFoodLogsAsync.pending, (state) => {
state.status = "loading";
});
},
});
export const selectFoods = (state) => state.foodLog;
export default foodsLogSlice.reducer;
This will get the code running but if anyone knows the underlying issue, I would accept that answer because it is 'more correct.' I believe this answer just bypasses Immer in Redux Toolkit, not exactly ideal.
const foodsLogSlice = createSlice({
name: "foodsLogged",
initialState,
reducers: {},
extraReducers: (builder) => {
builder
.addCase(getFoodLogsAsync.fulfilled, (state, action) => {
//similar error to the link below,
//status: "idle" causes immer error.
//http://5.9.10.113/67418074/redux-thunk-modifying-state-unhandled-promise-rejection-error-immer-immer
return (state = {
...state,
status: "idle",
foodLogs: action.payload,
});
})
.addCase(getFoodLogsAsync.rejected, (state, action) => {
return (state = {
...state,
status: "failed",
});
})
.addCase(getFoodLogsAsync.pending, (state) => {
return (state = {
...state,
status: "loading",
});
});
},
});
I am using redux-thunk and had this same issue. Tried several things but couldn't get it to work.
I had the idea of running it in a private window. I logged in and opened the page where this error occurred (on my fetchUserTypes action) and it worked fine.
I realized it is caused by some cached data and that is when I had the idea to clear my local storage (I am using redux-thunk which stores the redux state in local storage).
This will not result in an error if you only use JavaScript - seems like it only happens when you use TypeScript and you modify the structure of your reducer.
I am leaving this here because this has happened to me twice and I opened this link both times and in both cases I was able to solve the problem only after clearing local storage. Next time I decide to modify the structure of my reducer and run into this error I will be one step ahead :).
Related
Take a look at my Slice below
import { createSlice, createAsyncThunk } from "#reduxjs/toolkit";
import axios from "axios";
const KEY = process.env.REACT_APP_API_KEY
const BASE_URL = process.env.REACT_APP_BASE_URL
const API = `${BASE_URL}/countrymap`
const initialState = {
mapdata:[],
status: 'idle',
error:null
}
export const fetchMapData = createAsyncThunk(
'mapdata/fetchMapData',
async (id) => {
try {
const response = await axios.get(
API,
{
headers: {
'Content-Type': 'application/json',
'X-API-KEY': KEY,
},
params: {
titleId: id,
}
}
)
return response.data.Item;
} catch (error) {
console.error('API call error:', error.message);
}
}
)
const mapSlice = createSlice({
name: 'mapdata',
initialState,
reducers:{
},
extraReducers(builder) {
builder
.addCase(fetchMapData.fulfilled, (state, action) => {
state.status = 'succeeded'
//This attempt to make it an array failed with the error
// that the state is not iterable
state.mapdata = [action.payload, ...state]
console.log("map payload: ", state.mapdata);
})
}
})
// SELECTORS
// This works for an array, but the data is originally coming
// in as an object instead
export const allMapData = (state, id) => state.mapdata.mapdata.find(item => item.title_id === id);
export default mapSlice.reducer
for reference, look at these two console logs from two different API calls to two different endpoints from two different slices . Except Media is coming back as an array, and map is an object
I need to either, turn the state.mapdata into an Object to I can use the selector the way it is or re-code the selector so that it doesn't use the .find() function because that's an array function. But either way, it needs to compare the titleId in the data to the id in the params
Sorry for not providing workable code. I would but there is an insane amount of dependencies here
You should use
state.mapdata = [action.payload, ...state.mapdata]
instead of
state.mapdata = [action.payload, ...state]
I am having trouble with my Redux store in cases where I am passing params through a Thunk action. In cases were there is no param, my store is populating correctly. The action is completing successfully and I can see that the data has been returned to the front end by the success / fulfilled message of my action but there is no sign of it going into the store as state.
I had an instance previously where the array list was named incorrectly from the backend however this is not the case this time.
Is there anything that stands out why my store isn't populating with the state data?
action
export const requireUserDiveLogData = createAsyncThunk(
'users/requireData', // action name
// action expects to be called with the name of the field
async (userId) => {
// you need to define a function to fetch the data by field name
const response = await userDiveLogList(userId);
// what we return will be the action payload
return response.data;
},
// only fetch when needed: https://redux-toolkit.js.org/api/createAsyncThunk#canceling-before-execution
{
// _ denotes variables that aren't used - the first argument is the args of the action creator
condition: (_, { getState }) => {
const { users } = getState(); // returns redux state
// check if there is already data by looking at the didLoadData property
if (users.didLoadDiveLogData) {
// return false to cancel execution
return false;
}
}
}
)
reducer
export const userSlice = createSlice({
name: 'users',
initialState: {
userDiveLogList: [],
didLoadDiveLogData: false,
},
reducers: {
[requireUserDiveLogData.pending.type]: (state) => {
state.didLoadDiveLogData = true;
},
[requireUserDiveLogData.fulfilled.type]: (state, action) => {
return {
...state,
...action.payload
}
},
}
})
You should use extraReducers rather than reducers to handle actions produced by createAsyncThunk and createAction functions.
Besides, Redux Toolkit's createReducer and createSlice automatically use Immer internally to let you write simpler immutable update logic using "mutating" syntax. You don't need to do the shallow copy work by yourself.
E.g.
// #ts-nocheck
import {
configureStore,
createAsyncThunk,
createSlice,
} from '#reduxjs/toolkit';
async function userDiveLogList(userId) {
return { data: { userDiveLogList: [1, 2, 3] } };
}
export const requireUserDiveLogData = createAsyncThunk(
'users/requireData',
async (userId) => {
const response = await userDiveLogList(userId);
return response.data;
},
{
condition: (_, { getState }) => {
const { users } = getState();
if (users.didLoadDiveLogData) {
return false;
}
},
}
);
const userSlice = createSlice({
name: 'users',
initialState: {
userDiveLogList: [],
didLoadDiveLogData: false,
},
reducers: {},
extraReducers: (builder) => {
builder
.addCase(requireUserDiveLogData.pending, (state) => {
state.didLoadDiveLogData = true;
})
.addCase(requireUserDiveLogData.fulfilled, (state, action) => {
state.userDiveLogList = action.payload.userDiveLogList;
});
},
});
const store = configureStore({
reducer: {
users: userSlice.reducer,
},
});
store.dispatch(requireUserDiveLogData()).then(() => {
console.log(JSON.stringify(store.getState(), null, 2));
});
Output in the console:
{
"users": {
"userDiveLogList": [
1,
2,
3
],
"didLoadDiveLogData": true
}
}
I have a function "sendMessage" in React class:
class MessageForm extends React.Component {
...
sendMessage = async () => {
const { message } = this.state;
if (message) {
this.setState({ loading: true });
if (this.props.isPrivateChannel === false) {
socket.emit("createMessage", this.createMessage(), (response) => {
this.setState({ loading: false, message: "", errors: [] });
});
} else {
if (this.state.channel && this.state.channel._id === undefined) {
socket.emit("createChannelPM", this.state.channel, async (response) => {
const chInfo = { ...response, name: this.props.currentChannel.name };
console.log("chInfo : ", chInfo);
await this.props.setCurrentChannel(chInfo).then((data) => {
if (data) {
console.log("data : ", data);
console.log("this.props.currentChannel : ", this.props.currentChannel);
}
});
});
}
...
function mapStateToProps(state) {
return {
isPrivateChannel: state.channel.isPrivateChannel,
currentChannel: state.channel.currentChannel,
};
}
const mapDispatchToProps = (dispatch) => {
return {
setCurrentChannel: async (channel) => await dispatch(setCurrentChannel(channel)),
}
};
Here, in sendMessage function, I retrieve "response" from socket.io, then put this data into variable "chInfo" and assign this to Redux state, then print it right after assinging it.
And Redux Action function, "setCurrentChannel" looks like:
export const setCurrentChannel = channel => {
return {
type: SET_CURRENT_CHANNEL,
payload: {
currentChannel: channel
}
};
};
Reducer "SET_CURRENT_CHANNEL" looks like:
export default function (state = initialState, action) {
switch (action.type) {
case SET_CURRENT_CHANNEL:
return {
...state,
currentChannel: action.payload.currentChannel
};
...
The backend Socket.io part look like (I use MongoDB):
socket.on('createChannelPM', async (data, callback) => {
const channel = await PrivateChannel.create({
...data
});
callback(channel)
});
The console.log says:
Problem : The last output, "this.props.currentChannel" should be same as the first output "chInfo", but it is different and only print out previous value.
However, in Redux chrome extension, "this.props.currentChannel" is exactly same as "chInfo":
How can I get and use newly changed Redux states immediately after assinging it to Redux State?
You won't get the updated values immediately in this.props.currentChannel. After the redux store is updated mapStateToProps of MessageForm component is called again. Here the state state.channel.currentChannel will be mapped to currentChannel. In this component you get the updated props which will be accessed as this.props.currentChannel.
I believe you want to render UI with the latest data which you which you can do.
I'm working a side project using react, redux and firebase as backend.
Right now I was looking a way to store the URL of the image I was uploading into my firestore database along side other data.
This is what is working at the moment:
Action addProduct.js
export const addProduct = (product) => {
return (dispatch, getState, { getFireStore }) => {
const db = firestore;
const uploadTask = storage.ref(`images/${product.image.name}`).put(product.image);
uploadTask.on(
"state_changed",
snapshot => {},
error => {
console.log(error);
},
() => {
storage
.ref("images")
.child(product.image.name)
.getDownloadURL()
.then(url => {
let updatedProduct = {
...product,
image: url
}
db.collection('products').add({
...updatedProduct,
}).then(() => {
dispatch({
type: ADD_PRODUCT,
updatedProduct
})
}).catch((err) => {
dispatch({ type: ADD_PRODUCT_ERR, err })
})
})
}
)
}
Couple of things bugging me here:
While it successfully uploads the image and stores its url in the database, this code looks really messy from my perspective. Can an action file be this complex? Is there a more cleaner way to do this?
My reducer is returning an undefined payload for some reason I can't find.
Reducer productReducer.js
import { ADD_PRODUCT, ADD_PRODUCT_ERR } from "../actions/types"
const initialState = {
product: null
}
export default (state = initialState, action) => {
switch(action.type) {
case ADD_PRODUCT:
console.log('Product Added', action.product)
return state
case ADD_PRODUCT_ERR:
console.log('Product Failed', action.err)
return state
default:
return state
}
}
const FETCH_USERS_REQUEST = 'FETCH_USERS_REQUEST'
const FETCH_USERS_SUCCESS = 'FETCH_USERS_SUCCESS'
const FETCH_USERS_FAILURE = 'FETCH_USERS_FAILURE'
Below code shows no error but it does not work. I dont see any mistakes here as i am new to Redux. So i request anyone to look into this and find out why output is not printed to console. There might be something very basic that i am missing here. Please check.please check image for remaining code.
//Reducer function
const reducer = (state=initialState,action)=>{
switch(action.type)
{
case 'FETCH_USERS_REQUEST':
return {
...state,
loading:true
}
case 'FETCH_USERS_SUCCESS':
return {
...state,
loading:false,
users: action.payload,
error:''
}
case 'FETCH_USERS_FAILURE':
return {
...state,
loading:false,
users: [],
error:action.payload
}
}
}
//common channel
const fetchUsers = () => {
return function(dispatch){
dispatch(fetchUsersRequest())
axios.get('https://jsonplaceholder.typicode.com/users')
.then(response => {
const users = response.data.map(user => user.id)
dispatch(fetchUsersSuccess(users))
})
.catch(error => {
dispatch(fetchUsersFailure(error.message))
})
}
}
const store = createStore(reducer, applyMiddleware(thunkMiddleware))
console.log("Debugging")
store.subscribe(() => {console.log((store.getState()))})
console.log("post subscribe")
store.dispatch(fetchUsers)
Issue is on this line:
store.dispatch(fetchUsers)
You must actually invoke the function like this to get it to work:
store.dispatch(fetchUsers())