I created a Todo application in React.js. I have an API to store my todos and their states.
In my application I can drag and drop the todos in the state I want and I can also CRUD the states.
For the state part I use React-Redux.
My problem happens when adding a new state, I would like to get the id that is provided by the API but in my reducer I get this error: A non-serializable value was detected in the state, in the path: stateReducer.
If I don't get the id, I can't delete the state cause I delete by id, I have to reload the page.
To initialize my state list I use a middleware to do the asymmetric part but I don't understand how to do it when I add a new object.
import { createAsyncThunk, createSlice } from "#reduxjs/toolkit";
import Api from "../api/Api.ts";
const api = new Api("http://localhost:3333/api/state/")
const initialState = {
status:'idle',
list:[],
error: null
}
export const StateSlice = createSlice({
name:'status',
initialState,
reducers: {
add: async (state, action) => {
let item
await api.post(action.payload).then(state => item = state) // PROBLEME
state.list.push(item)
},
remove:(state, action) => {
state.list.splice(state.list.indexOf(action.payload),1)
api.del(action.payload)
},
update:(state, action) => {
api.update(action.payload)
},
get: async (state,action) => {
state.list = action.payload
},
},
extraReducers(builder) {
builder
.addCase(fetchPosts.pending, (state, action) => {
state.status = 'loading'
})
.addCase(fetchPosts.fulfilled, (state, action) => {
state.status = 'succeeded'
state.list = state.list.concat(action.payload)
})
.addCase(fetchPosts.rejected, (state, action) => {
state.status = 'failed'
state.error = action.error.message
})
}
})
const getPosts = async () => {
const states = []
let posts = await api.get()
posts.forEach(p => {
states.push({id:p.id,name:p.name})
});
return states
}
export const fetchPosts = createAsyncThunk('state/fetchPosts', async () => {
let response = await getPosts()
return response
})
export const {add, remove, update, get} = StateSlice.actions
export default StateSlice.reducer;
Do I need to create another middleware for this and how call it ? I am new to this technology, can you give me a hint please?
Related
I have an initialState and a dynamic URL and async thunk for axios
I want to change URL query parameter according to a key in initial state.
?query=${initialState.category}
category changes but URL fetches data with unchanged category (it uses initial key not modified key)
const initialState = {
category: "burger",
products: [],
isLoading: true,
categories :{
burger:"burger",
kebab:"kebab",
chicken:"chicken",
pizza:"pizza",
fish:"fish",
vegan:"vegan",
salad:"salad",
pasta:"pasta",
steak:"steak",
dessert:"dessert",
waffle:"waffle"
}
};
const url = `https://api.spoonacular.com/food/menuItems/search?query=${initialState.category}&number=10&apiKey=API_KEY`;
export const getProducts = createAsyncThunk(
"products/getProducts",
async (_, thunkAPI) => {
try {
const response = await axios(url);
console.log(response);
return await response.data;
} catch (error) {
return thunkAPI.rejectWithValue({ error: error.message });
}
}
);
export const productSlice = createSlice({
name: "product",
initialState,
reducers: {
pickCategory: (state, action) => {
state.category = action.payload;
},
that line is static as value in initialState is not mutated:
const url = `https://api.spoonacular.com/food/menuItems/search?query=${initialState.category}&number=10&apiKey=API_KEY`;
you need to move that constant inside getProducts thunk as thunk has an access to current store:
...
async (_, thunkAPI) => {
const {category} = thunkApi.getState()
const url = `https://api.spoonacular.com/food/menuItems/search?
query=${category}&number=10&apiKey=API_KEY`;
}
...
Can someone please help me to find what exact problem is with below code, as i am new in redux.
API is not called
data is not coming to state products[] array
product-slice.js
import { createAsyncThunk, createSlice } from "#reduxjs/toolkit";<br/>
import axios from "axios";<br/>
const initialState = {<br/>
products: [],<br/>
sample: "Hello World",<br/>
};<br/>
<br/>
const productSlice = createSlice({<br/>
name: "productSlice",<br/>
initialState,<br/>
reducers: {<br/>
getProducts(state, action) {<br/>
const p = action.payload;<br/>
console.log(p);<br/>
},<br/>
},<br/>
extraReducers: (builder) => {<br/>
builder.addCase(fetchProductData.fulfilled, (state, action) => {<br/>
state.products.push(action.payload);<br/>
});<br/>
},<br/>
});<br/>
export const fetchProductData = createAsyncThunk(<br/>
"products/fetchProducts",<br/>
async (_, thunkAPI) => {<br/>
try {<br/>
const response = await axios.get("http://localhost:8080/products");<br/>
return await response.data;<br/>
} catch (error) {<br/>
return error;<br/>
}<br/>
}<br/>
);<br/>
export const productActions = productSlice.actions;<br/>
export default productSlice;<br/>
ProductList.js(Component)<br/>
const ProductList = (props) => {<br/>
const history = useHistory();<br/>
const hello = useSelector((state) => state.products.sample);<br/>
const dispatch = useDispatch();<br/>
<br/>
useEffect(() => {<br/>
dispatch(fetchProductData);<br/>
}, [dispatch]);<br/>
You should call an action as a function inside dispatch() like dispatch(fetchProductData()) and implement redux/toolkit with thunk middleware like below (example):
as you can see in reduxToolkitCreateAsyncThunk
import { createAsyncThunk, createSlice } from '#reduxjs/toolkit'
import { userAPI } from './userAPI'
// First, create the thunk
const fetchUserById = createAsyncThunk(
'users/fetchByIdStatus',
async (userId, thunkAPI) => {
const response = await userAPI.fetchById(userId)
return response.data
}
)
// Then, handle actions in your reducers:
const usersSlice = createSlice({
name: 'users',
initialState: { entities: [], loading: 'idle' },
reducers: {
// standard reducer logic, with auto-generated action types per reducer
},
extraReducers: (builder) => {
// Add reducers for additional action types here, and handle loading state as needed
builder.addCase(fetchUserById.fulfilled, (state, action) => {
// Add user to the state array
state.entities.push(action.payload)
})
},
})
// Later, dispatch the thunk as needed in the app
dispatch(fetchUserById(123))
I have the following function to check the users
export const authCheckState = () => {
return dispatch => {
const token = localStorage.getItem("token");
const email = localStorage.getItem("email");
if (token === undefined) {
dispatch(logout());
} else {
const expirationDate = new Date(localStorage.getItem("expirationDate"));
if (expirationDate <= new Date()) {
dispatch(logout());
} else {
dispatch(authSuccess(email, token));
dispatch(
checkAuthTimeout(
(expirationDate.getTime() - new Date().getTime()) / 1000
)
);
}
}
};
};
And for that i did the special initialization function that will take all the needed functions and dispatch them
initialState = {
initialized: false
};
const appReducer = (state = initialState, action) => {
switch (action.type) {
case INITIALIZED_SUCCESS:
return {
...state,
initialized: true
}
default:
return state;
}
}
export const initializedSuccess = () => ({type: INITIALIZED_SUCCESS});
export const initializeApp = () => (dispatch) => {
let promise = dispatch(authCheckState());
Promise.all([promise])
.then(() => {
dispatch(initializedSuccess());
});
}
And after that i am putting initialization function to the App's componentDidMount function
componentDidMount() {
this.props.initializeApp();
}
and after i am doing that
const mapStateToProps = (state) => ({
initialized: state.app.initialized
})
but after that i am still getting the data too late and i have to refresh the page to see the data. How to implement this feature?
You should fetch data in the component itself. Use Redux for storing the data so that you can use it later or manipulate it. (If you need to do so!)
This how the process should be,
Make an API request on componentDidMount.
Store the data in redux.
Use that data in the component coming in the form of props using Redux.
I'm learning Redux-Saga and everything works well with { configureStore, getDefaultMiddleware, createAction, createReducer }. However, I cannot successfully implement createSlice.
My actions seem to be dispatched just fine (though I'm not sure since I have multiple Redux stores and placing console.log inside createSlice doesn't seem to work...). I just cannot get the store values - after dispatched action the relevant state value (initially '') becomes undefined. I did wrap my component inside Provider and all. Can someone enlighten me how does createSlice work? Thanks.
RESOLVED I had a bug somewhere else in my code, that's why the reducers weren't working proberly. BUT what I was asking about and what was causing my problems is this: actions passed to createSlice must be 'pure' functions, meaning: (state, action) -> state, nothing fancy. That's why I had to remove my fetching functions (getData1 and getData2) from this createSlice.
ComponentWrapper returns this
<Provider store={toolkitCreateSliceStore}>
<ReduxToolkitCreateSliceComponent />
</Provider>
Component (Buttons just dispatch actions)
class ReduxToolkitCreateSliceComponent extends React.Component {
render () {
return (
<>
<h2>
{this.props.data1}
{(this.props.data1!=='' && this.props.data2!=='') ? ', ' : ''}
{this.props.data2}
</h2><br/>
<h3>{this.props.message}</h3>
<Button1 />
<Button2 />
<Button3 />
</>
);
}
}
function mapStateToProps(state) {
return {
data1: state.toolkitCreateSliceReducer.data1,
data2: state.toolkitCreateSliceReducer.data2,
message: state.toolkitCreateSliceReducer.message
};
}
export default connect(mapStateToProps)(ReduxToolkitCreateSliceComponent);
Redux Toolkit slice
import { createSlice } from "#reduxjs/toolkit";
import axios from "axios";
const initialSliceState = {
data1: '',
data2: '',
message: ''
};
const slice = createSlice({
name: "slice",
initialState: initialSliceState,
reducers: {
getData1: (state, action) => {
return dispatch => {
dispatch(loading1());
return axios.get('http://localhost:8081/data1')
.then(function (response) {
if (response.status === 200) {
dispatch(setResponse1(response.data));
}
}).catch(error => dispatch(displayError1(error)));
};
},
getData2: (state, action) => {
return dispatch => {
dispatch(loading2());
return axios.get('http://localhost:8081/data2')
.then(function (response) {
if (response.status === 200) {
dispatch(setResponse2(response.data));
}
}).catch(error => dispatch(displayError2(error)));
};
},
setResponse1: (state, action) => {
state.data1 = action.payload;
state.message = 'success';
},
setResponse2: (state, action) => {
state.data2 = action.payload;
state.message = 'success';
},
reset: (state, action) => {
state.data1 = '';
state.data2 = '';
state.message = 'reset';
},
loading1: (state, action) => {
state.message = 'loading';
},
loading2: (state, action) => {
state.message = 'loading';
},
displayError1: (state, action) => {
state.message = action.payload;;
},
displayError2: (state, action) => {
state.message = action.payload;;
}
}
});
export const toolkitCreateSliceReducer = slice.reducer;
const { getData1, getData2, setResponse1, setResponse2, reset, loading1, loading2,
displayError1, displayError2} = slice.actions;
export default slice;
Redux Toolkit store
const middleware = [
...getDefaultMiddleware()
];
const toolkitCreateSliceStore = configureStore({
reducer: {
toolkitCreateSliceReducer
},
middleware
});
export default toolkitCreateSliceStore;
Your "reducers" are very wrong.
A reducer must never have any side effects like AJAX calls.
You've written some Redux "thunk" functions where your reducers should be:
getData1: (state, action) => {
return dispatch => {
dispatch(loading1());
return axios.get('http://localhost:8081/data1')
.then(function (response) {
if (response.status === 200) {
dispatch(setResponse1(response.data));
}
}).catch(error => dispatch(displayError1(error)));
};
},
This is a thunk, not a reducer.
A reducer would be something like:
getData(state, action) {
return action.payload;
}
I'd specifically recommend reading through our brand-new "Redux Essentials" core docs tutorial, which teaches beginners "how to use Redux, the right way", using our latest recommended tools and practices like Redux Toolkit. It specifically covers how reducers should work, how to write reducers with createSlice, and how to write and use thunks alongside createSlice:
https://redux.js.org/tutorials/essentials/part-1-overview-concepts
If you're interested in creating async actions, let me recommend you an npm package that I created and use. It is saga-toolkit that allows async functions to get resolved by sagas.
slice.js
import { createSlice } from '#reduxjs/toolkit'
import { createSagaAction } from 'saga-toolkit'
const name = 'example'
const initialState = {
result: null,
loading: false,
error: null,
}
export const fetchThings = createSagaAction(`${name}/fetchThings`)
const slice = createSlice({
name,
initialState,
extraReducers: {
[fetchThings.pending]: () => ({
loading: true,
}),
[fetchThings.fulfilled]: ({ payload }) => ({
result: payload,
loading: false,
}),
[fetchThings.rejected]: ({ error }) => ({
error,
loading: false,
}),
},
})
export default slice.reducer
sagas.js
import { call } from 'redux-saga/effects'
import { takeLatestAsync } from 'saga-toolkit'
import API from 'hyper-super-api'
import * as actions from './slice'
function* fetchThings() {
const result = yield call(() => API.get('/things'))
return result
}
export default [
takeLatestAsync(actions.fetchThings.type, fetchThings),
]
I'm using Redux Toolkit to connect to an API with Axios.
I'm using the following code:
const products = createSlice({
name: "products",
initialState: {
products[]
},
reducers: {
reducer2: state => {
axios
.get('myurl')
.then(response => {
//console.log(response.data.products);
state.products.concat(response.data.products);
})
}
}
});
axios is connecting to the API because the commented line to print to the console is showing me the data. However, the state.products.concat(response.data.products); is throwing the following error:
TypeError: Cannot perform 'get' on a proxy that has been revoked
Is there any way to fix this problem?
I would prefer to use createAsyncThunk for API Data with extraReducers
Let assume this page name is productSlice.js
import { createSlice,createSelector,PayloadAction,createAsyncThunk,} from "#reduxjs/toolkit";
export const fetchProducts = createAsyncThunk(
"products/fetchProducts", async (_, thunkAPI) => {
try {
//const response = await fetch(`url`); //where you want to fetch data
//Your Axios code part.
const response = await axios.get(`url`);//where you want to fetch data
return await response.json();
} catch (error) {
return thunkAPI.rejectWithValue({ error: error.message });
}
});
const productsSlice = createSlice({
name: "products",
initialState: {
products: [],
loading: "idle",
error: "",
},
reducers: {},
extraReducers: (builder) => {
builder.addCase(fetchProducts.pending, (state) => {
state. products = [];
state.loading = "loading";
});
builder.addCase(
fetchProducts.fulfilled, (state, { payload }) => {
state. products = payload;
state.loading = "loaded";
});
builder.addCase(
fetchProducts.rejected,(state, action) => {
state.loading = "error";
state.error = action.error.message;
});
}
});
export const selectProducts = createSelector(
(state) => ({
products: state.products,
loading: state.products.loading,
}), (state) => state
);
export default productsSlice;
In your store.js add productsSlice: productsSlice.reducer in out store reducer.
Then for using in component add those code ... I'm also prefer to use hook
import { useSelector, useDispatch } from "react-redux";
import {
fetchProducts,
selectProducts,
} from "path/productSlice.js";
Then Last part calling those method inside your competent like this
const dispatch = useDispatch();
const { products } = useSelector(selectProducts);
React.useEffect(() => {
dispatch(fetchProducts());
}, [dispatch]);
And Finally, you can access data as products in your component.
It is happening because your reducer function is not a pure function, it should not be having any asynchronous calls.
something like this should work. it will fix the error you are getting
const products = createSlice({
name: "products",
initialState: {
products: []
},
reducers: {
reducer2: (state, { payload }) => {
return { products: [...state.products, ...payload]}
})
}
}
});
and api should be called outside
const fetchProducts = () => {
axios.get('myurl')
.then(response => {
//console.log(response.data.products);
store.dispatch(products.actions.reducer2(response.data.products))
})
};
PS: haven't tried running above code, you may have to make modifications as per your need.