Immer - Get error with async function in produce - javascript

I'm trying to create an asynchronous produce with immer but I get an error when I call this async function :
This my code :
import { combineReducers, createStore } from 'redux';
import produce from 'immer';
const mainReducer = produce(async (draft, { type, payload }: { type: string; payload: any }) => {
switch (type) {
case 'foo': {
draft = await myAsyncFn(payload);
}
}
});
const reducers = {
main: mainReducer,
};
const rootReducer = combineReducers(reducers);
export const mainStore = createStore(rootReducer);
This is the output : Error: [Immer] produce can only be called on things that are draftable: plain objects, arrays, Map, Set or classes that are marked with '[immerable]: true'. Got '[object Promise]'
Why doesn't it work ? (I thought It's was possible : https://immerjs.github.io/immer/docs/async)
What does it mean by classes that are marked with '[immerable] ?

While Immer does appear to let you write async logic inside of produce, you must never write async logic in a Redux reducer.
Having said that, the specific error here is because by default, Immer only knows how to update plain JS objects and arrays. In order to update a class instance or similar, you have to add a special immerable symbol to that class type.
Note that you should be using Immer with Redux, but as part of our official Redux Toolkit package, which has Immer built into its createSlice API.

Might not be so relevant but I got this error using redux-toolkit. The createSlice() method was working well until I changed the type of the state to a generic typescript type. Then it started throwing the mentioned error.
import { AsyncState } from "./AsyncState";
export class SliceState<T> {
constructor(initialState: T) {
this.data = initialState;
this.status = new AsyncState();
}
status: AsyncState;
data: T;
}
Additional error information: [Immer] produce can only be called on things that are draftable: plain objects, arrays, Map, Set or classes that are marked with '[immerable]: true'. Got '[object Object]'

Related

How to set state to api data in the store

I am trying to set my state to the data I'm getting from my API with a GETTER in the store.
during the mounted() lifecyclehook trigger the GETTER getProducts() which looks like this:
export const getters = {
async getProducts() {
axios.get('/api/products')
.then(res => {
var data = res.data
commit('setProducts', data)
})
.catch(err => console.log(err));
}
}
In the GETTER I try to trigger a MUTATION called setProducts() which looks like this:
export const mutations = {
setProducts(state, data) {
state.products = data
}
}
But when I run this I get the error ReferenceError: commit is not defined in my console.
So obviously what goes wrong is triggering the MUTATION but after looking for 2 days straight on the internet I still couldn't find anything.
I also tried replacing commit('setProducts', data) with:
this.setProducts(data)
setProducts(data)
Which all ended with the error "TypeError: Cannot read properties of undefined (reading 'setProducts')"
If your function getProduct is defined in a Vue component, you have to access the store like this :
this.$store.commit('setProducts', data)
If your function is not defined in a Vue component but in an external javascript file, you must first import your store
import store from './fileWhereIsYourStore.js'
store.commit('setProducts', data)
If your getters export is literally the definition of your store's getters, you can use the solution of importing the store first, but you should know that it is clearly not a good practice to make commits in getters. There must be a better solution to your problem.
EDIT : To answer your comment, here's how you could do it:
// Your store module
export default {
state: {
products: []
},
mutations: {
SET_PRODUCTS(state, data) {
state.products = data
}
},
actions: {
async fetchProducts(store) {
await axios.get('/api/products')
.then(res => {
var data = res.data
store.commit('SET_PRODUCTS', data)
})
.catch(err => console.log(err));
}
}
}
Now, you can fetch products and populate your store in each of your components like this :
// A random Vue Component
<template>
</template>
<script>
export default {
async mounted() {
await this.$store.dispatch('fetchProducts')
// now you can access your products like this
console.log(this.$store.state.products)
}
}
</script>
I didn't tested this code but it should be ok.
Only actions do have commit in their context as you can see here.
Getters don't have commit.
Otherwise, you could also use mapActions (aka import { mapActions } from 'vuex'), rather than this.$store.dispatch (just a matter of style, no real difference at the end).
Refactoring your code to have an action as Julien suggested is a good solution because this is how you should be using Vuex.
Getters are usually used to have some state having a specific structure, like sorted alphabetically or alike. For common state access, use the regular state or the mapState helper.

Methods missing from Javascript Class inside Vuex state

I'm using Nuxt framework alongside Vuex to store data in my web site but I'm facing trouble when I want to use a class directly in the state.
With a model cart.js defined like this:
export class Cart {
constructor(ownedID) {
this._created = new Date();
this._lastUpdated = new Date();
this._ownerID = ownedID || 'visitor'
this._items = []
}
getItem (articleNumber) {
console.log(this._items)
}
...
}
And my store's module cart.js
import { Cart } from "~/models/cart";
const state = () => ({
cart: new Cart()
})
const mutations = {
ADD_ITEM(state, newItem) {
console.log(state.cart)
}
}
...
When the ADD_ITEM(state, newItem) mutation is called the getItem(articleNumber) function is missing and thus I receive the TypeError: state.cart.getItem is not a function error.
This is the result of the console.log:
__ob__: Object { value: {…}, dep: {…}, vmCount: 0 }
​
_created:
_item:
_lastUpdated:
_ownerID:
This is a sandbox link of my setup.
Nuxt vuex sandbox error
Does anyone have a clue about my issue.
Thank you.
Vue accepts only plain objects & Observes only native object properties, It ignores the prototype properties. According to the Vue documentation
The object must be plain: native objects such as browser API objects and prototype properties are ignored
In your case, your using a class which creates variables in plain object and methods in prototype(__proto__), That's why state is unable to find getItem method. You need to use plain objects instead of classes
.

Use ES6 classes as vuex store state in Nuxt

When I set ES6 class to state on my vuex store in nuxt I got following warn:
WARN Cannot stringify arbitrary non-POJOs EndPoint
and When I use object as state is works without warning.
So How can I use ES6 classes in my state.
My model:
export default class EndPoint {
constructor(newEndPoints) {
this.login = newEndPoints.login;
this.status = newEndPoints.status;
}
}
and mutate state here:
commit(CoreMutations.SET_ENDPOINTS, new EndPoint(response.data));
Using object:
const EndPoint = {
endPoints(newEndPoints) {
return {
login: newEndPoints.login,
status: newEndPoints.status
};
}
};
and mutate:
commit(CoreMutations.SET_ENDPOINTS, EndPoint.endPoints(response.data));
As discussion in comment add toJSON method in class solve the problem when setting state in client, but when set state in server, states will become undefined.
so adding this code solve the problem:
toJSON() {
return Object.getOwnPropertyNames(this).reduce((a, b) => {
a[b] = this[b];
return a;
}, {});
}

import props, destructuring assignment & unresolved variable warning

I'm a bit new at frontend, so I have a question about code practices with with props importing.
I'm using next.js (which is based on React) and I'm need to insert props from API endpoint right to my page.
According to the example, it should looks like this:
export async function getServerSideProps({query}) {
const res = await fetch(encodeURI(`url_here+${query}`));
const json = await res.json();
The problem is with «what happens» next:
If I export my props (result of the function above) to page like this:
return { props: {
_id: json._id,
ilvl: json.ilvl,
...
checksum: json.checksum,
And import it like with using of destructuring assignment as an argument function:
function CharacterPage({ _id, id, ... }) {
...
}
THE PROBLEM
That there are almost 16+ key:values in response json object from API endpoint.
So if I will follow to the codestyle from above that will be.. em.. guess you already understand.
So I could export result from API endpoint like:
export async function getServerSideProps({query}) {
const res = await fetch(encodeURI(`url_here`));
const json = await res.json();
return {props: {json}
}
And import it, as one argument to the page like:
function CharacterPage({json})
But if I'll use json.name object keys on page (for conditional rendering) my IDE (WebStrom) shows me unresolved variable warning.
So where can I read about correct import practice and find react-import props example with lots of keys from JSON?
Should I use:
let {id, name, ...etc} = json
right after:
function CharacterPage({json})
for every key that I want to access or there is a better way/code practice for importing props?
My First idea is you can modify the JSON object in return of getServerSideProps. It would be more clear to identify which kind of object with attributes used here.
return { props: {
name: json.name,
id: json.id
...
}
}
If you cannot do that, it would be better to destructure initially.
let {id, name, ...etc} = json
But only destructure the elements you need. There is no need for destructuring all the elements.

Redux: using thunk middleware and combineReducers introduces extra key to getState

Problem: When using thunk middleware before introducing Redux.combineReducers, the getState passed to the thunk correctly returns an object with the correct keys. After refactoring to use Redux.combineReducers, the getState passed to the thunk now returns an object with nested keys. See code below which (hopefully) illustrates my point. This could lead to a potential maintenance nightmare of having to constantly grab the correct key for any thunk method that accesses state.
Question: Is there a simple way to set the correct context key within the thunk? The code feels brittle when I combine reducers and have to insert keys to access the correct state. Am I missing something simple?
Before code:
const Redux = require('redux'),
Thunk = require('redux-thunk');
// this is an action generator that returns a function and is handled by thunk
const doSomethingWithFoo = function() {
return function(dispatch, getState) {
// here we're trying to get state.fooValue
const fooValue = getState().fooValue;
dispatch({ type: "DO_SOMETHING", fooValue });
}
};
// this is a simple action generator that returns a plain action object
const doSimpleAction = function(value) {
// we simply pass the value to the action.
// we don't have to worry about the state's context at all.
// combineReducers() handles setting the context for us.
return { type: "SIMPLE_ACTION", value };
}
const fooReducer(state, action) {
// this code doesn't really matter
...
}
const applyMiddleware = Redux.applyMiddleware(Thunk)(Redux.createStore);
const fooStore = applyMiddleware(fooReducer);
After code (introducing a more global appStore):
// need to rewrite my thunk now because getState returns different state shape
const doSomethingWithFoo = function() {
return function(dispatch, getState) {
// here we're trying to get state.fooValue, but the shape is different
const fooValue = getState().foo.fooValue;
dispatch({ type: "DO_SOMETHING", fooValue });
}
};
const appReducers = Redux.combineReducers({
foo: fooReducer,
bar: barReducer,
});
const appStore = applyMiddleware(appReducers);
After thinking about it some more, I think the answer is to refactor the doSomethingWithFoo action generator so that it accepts fooValue as a parameter. Then I don't have to worry about state object shape changing.
const doSomethingWithFoo(fooValue) {
return function(dispatch, getState) {
// now we don't have to worry about the shape of getState()'s result
dispatch({ type: "DO_SOMETHING", fooValue });
}
}
You're over-thinking things. By definition, store.getState() returns the entire state, and combineReducers() pulls together multiple sub-reducers into a larger object. Both are working as intended. You're writing your own application, so you're responsible for how you want to actually organize your state shape and deal with it. If you feel things are too "brittle" this way, it's up to you to find a good way to structure things, but that's not a problem with Redux.
Also, using getState() in an action creator to determine what to do IS an entirely valid approach. In fact, the Reducing Boilerplate section of the Redux docs even does that as a demonstration:
export function addTodo(text) {
// This form is allowed by Redux Thunk middleware
// described below in “Async Action Creators” section.
return function (dispatch, getState) {
if (getState().todos.length === 3) {
// Exit early
return
}
dispatch(addTodoWithoutCheck(text))
}
}

Categories