I'm dispatching an action inside the component file to get filtered data from the Api and I want to get that filtered data with useSelector() but it's not working
Component.js
import React, { useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { apiRequestFilteredCourses } from '../../../actions';
const Courses = () => {
const categories = useSelector(state => state.categories);
const dispatch = useDispatch();
if (categories.isLoading || courses.isLoading) {
return (<h1>Loading</h1>);
}
const query = { category_id: 1 };
dispatch(apiRequestFilteredCourses(query));
filteredCoursesActions.js
import {
API_REQUEST_FILTERED_COURSES,
API_RECEIVE_FILTERED_COURSES,
} from '../constants/actions';
export const apiRequestFilteredCourses = data => (
{
type: API_REQUEST_FILTERED_COURSES,
payload: data,
}
);
export const apiReceiveFilteredCourses = data => ({ data, type: API_RECEIVE_FILTERED_COURSES });
filteredCoursesSagas.js
import { put, takeEvery } from 'redux-saga/effects';
import { apiReceiveFilteredCourses, apiRequestFilteredCourses } from '../actions';
import {
API_REQUEST_FILTERED_COURSES,
} from '../constants/actions';
import Api from '../services/Api';
// eslint-disable-next-line no-unused-vars
function* filterCourses(action) {
const query = new URLSearchParams(action.payload).toString();
try {
const { data, status } = yield Api.get(`courses?${query}`, false);
if (status === 200) {
yield put(apiReceiveFilteredCourses(data.data));
} else {
console.log(`Action with constant ${API_REQUEST_FILTERED_COURSES} Failed with status code ${status}`);
}
} catch (e) {
console.log('FROM COURSES', e);
}
}
export default function* filteredCoursesSagas() {
yield takeEvery(API_REQUEST_FILTERED_COURSES, filterCourses);
yield put(apiRequestFilteredCourses());
}
filteredCoursesReducer.js
import {
API_RECEIVE_FILTERED_COURSES, API_REQUEST_FILTERED_COURSES,
} from '../constants/actions';
const defaultState = {
isLoading: false,
filteredCourses: [],
isLoaded: false,
};
const filteredCoursesReducer = (state = defaultState, { type, data }) => {
switch (type) {
case API_RECEIVE_FILTERED_COURSES:
return {
...state,
filteredCourses: data,
isLoaded: true,
isLoading: false,
};
case API_REQUEST_FILTERED_COURSES:
return {
...state,
isLoading: true,
isLoaded: false,
filteredCourses: [],
};
default:
return state;
}
};
export default filteredCoursesReducer;
Api.js
static async get(url, throwErrors = false) {
const axiosConf = this.getConf();
try {
const response = await axios.get(url, axiosConf);
if (throwErrors && (response.status <= 100 || response.status >= 400)) {
throw new Error(`Art API Returned error status ${response.status}`);
}
return response;
} catch (e) {
Api.showError(e);
return e;
}
}
I tried to get the filteredCourses like I did with the categories which is working normally but it completely bugged the app.
const filteredCourses = useSelector(state => state.filteredCourses)
Your reducer does not seem to be handling API_REQUEST_FILTERED_COURSES, so of course, nothing will happen with your state.
Generally, just so you are aware: you are using an extremely outdated style of Redux here. Since 2019, we recommend using the official Redux Toolkit, which does not have ACTION_TYPES, switch..case reducers, hand-written action creators or immutable reducer logic with spread anymore. It is only 1/4 of the code. We also do not recommend using Redux-Saga anymore for most use cases - instead, you should be using RTK Query, createAsyncThunk, thunks, or the listenerMiddlware.
I would highly recommend you to read Why Redux Toolkit is how to use Redux today and then to take the official Redux tutorial. That old style of Redux you are using here is a pain and there is really no good reason to use that nowadays.
Related
im new to the redux toolkit here is my problem
im fetching data every time by creating own custom useFetch file for handling loading , success, error status
i used the same way with redux toolkit by creating createSlice method for accesing reducers and actions
this is working successfully and getting data from this way by dispaching actions and reducers
but i did't used the createAsyncThunk from redux toolkit
my confusion is is this currect way to fetch data from custom useFetch or should i use createAsyncthunk
Im not sure how to use createAsyncThunk in custom useFetch
if anyone knows the answer that is so appreciatable
posted my all files below
if i get answer with createAsyncThunk in custom useFetch that is soo appreciable
thanks advance
App.js
import React from "react";
import { useDispatch, useSelector } from "react-redux";
import useFetchData from "./Components/useFetchData";
import { actions } from "./Slices/CounterSlice";
const App = () => {
useFetchData("https://jsonplaceholder.typicode.com/todos");
const apiData = useSelector((s) => s);
console.log(apiData.api);
return (
<>
{apiData.api.status !== "success" && <h1>hello</h1>}
{apiData.api.status === "success" &&
apiData.api.apiData.map((el) => {
return <h6 key={el.id}>{el.title}</h6>;
})}
</>
);
};
export default App;
custom useFetchData.js file
import React, { useCallback } from "react";
import { useDispatch } from "react-redux";
import { ApiActions } from "../Slices/ApiSlice";
const useFetchData = (link) => {
const dispatch = useDispatch();
const fetchData = useCallback(async () => {
try {
dispatch(ApiActions.loadingTime());
const getData = await fetch(link);
const toJson = await getData.json();
dispatch(ApiActions.successTime(toJson));
} catch {
dispatch(ApiActions.errorTime());
}
}, [link]);
React.useEffect(() => {
fetchData();
}, [fetchData]);
};
export default useFetchData;
this is createSlice file for creating actions and reducers
import { createSlice } from "#reduxjs/toolkit";
const apiSlice = createSlice({
name: "api",
initialState: {
status: "idle",
apiData: [],
error: false,
},
reducers: {
loadingTime: (state, action) => {
state.status = "Loading";
},
successTime: (state, action) => {
state.apiData = action.payload;
state.status = "success";
},
errorTime: (state, action) => {
state.apiData = [];
state.status = "error";
},
},
});
export const ApiActions = apiSlice.actions;
export const apiReducers = apiSlice.reducer;
You can use one way or the other. Example with createAsyncThunk:
export const checkIfAuthenticated = createAsyncThunk(
"auth/checkIsAuth",
async (_, thunkAPI) => {
const response = await axios.post(
"http://127.0.0.1:8080/dj-rest-auth/token/verify/",
{ token: localStorage.getItem("access") }
);
thunkAPI.dispatch(getUserInfo());
return response.data;
}
);
Then you handle loading, success, error status as before:
extraReducers: (builder) => {
builder
.addCase(checkIfAuthenticated.fulfilled, (state, action) => {
state.isAuthenticated = true;
})
.addCase(checkIfAuthenticated.rejected, (state, action) => {
state.isAuthenticated = false;
snackbar({ error: "Nie jesteÅ› zalogowany" });
})
},
However, if you have a lot of API queries, the optimal solution is to use redux toolkit query or react-query. These two libraries make queries a lot better.
If you care about code cleanliness and ease, check them out.
I've been looking into creating generic modals with React, Redux, and Thunk. Ideally, my state would look like the following:
export interface ConfirmModalState {
isOpened: boolean;
onConfirm: null | Function
}
export const initialConfirmModalState: ConfirmModalState = {
isOpened: false,
onConfirm: null
};
However, this would mean putting non-serializable data into the state, which seems to be highly discouraged.
I've read a great blogpost by markerikson. However, I don't think the proposed solution would work with asynchronous actions and Thunk.
How do you suggest to resolve this issue?
I actually wrote the post that you linked, and I wrote a much-expanded version of that post a couple years later:
Practical Redux, Part 10: Managing Modals and Context Menus.
I've actually implemented a couple variations of this approach myself since I wrote that post, and the best solution I've found is to add a custom middleware that returns a promise when you dispatch a "show modal" action, and resolves the promise with a "return value" when the dialog is closed.
There's an existing implementation of this approach at https://github.com/AKolodeev/redux-promising-modals . I ended up making my own implementation. I have a partial version of my homegrown approach in a gist at https://gist.github.com/markerikson/8cd881db21a7d2a2011de9e317007580 , and the middleware looked roughly like:
export const dialogPromiseMiddleware: Middleware<DialogPromiseDispatch> = storeAPI => {
const dialogPromiseResolvers: Record<string, Resolver> = {};
return next => (action: AnyAction) => {
switch (action.type) {
// Had to resort to `toString()` here due to https://github.com/reduxjs/redux-starter-kit/issues/157
case showDialogInternal.toString(): {
next(action);
let promiseResolve: Resolver;
const dialogPromise = new Promise((resolve: Resolver) => {
promiseResolve = resolve;
});
dialogPromiseResolvers[action.payload.id] = promiseResolve!;
return dialogPromise;
}
case closeDialog.toString(): {
next(action);
const {id, values} = action.payload;
const resolver = dialogPromiseResolvers[id];
if (resolver) {
resolver(values);
}
delete dialogPromiseResolvers[id];
break;
}
default:
return next(action);
}
};
};
(note: I made that gist when I was having some TS syntax issues getting dispatching to work correctly, so it's likely it won't 100% work out of the box. RTK also now includes some .match() action matching utilities that would be useful here. but, it shows the basic approach.)
The rough usage in a component is:
const closedPromise = dispatch(showDialog("TestDialog", {dialogNumber : counter});
const result = await closedPromise
// do something with the result
That way you can write the "on confirm" logic write there in the place that asked for the dialog to be shown in the first place.
Thank you markerikson for providing an answer. This inspired me to create a solution with thunks. Please give me some feedback here :)
I will be using hooks and #reduxjs/toolkit in my example.
This is the state of my ConfirmationModal reducer:
export interface confirmationModalState {
isOpened: boolean;
isConfirmed: boolean;
isCancelled: boolean;
}
export const initialConfirmationModalState: confirmationModalState = {
isOpened: false,
isConfirmed: false,
isCancelled: false,
};
This is the slice (a combination of the reducer and actions):
import { createSlice } from '#reduxjs/toolkit';
import { initialConfirmationModalState } from './state';
const confirmationModalSlice = createSlice({
name: 'controls/confirmationModal',
initialState: initialConfirmationModalState,
reducers: {
open: state => {
state.isOpened = true;
state.isConfirmed = false;
state.isCancelled = false;
},
confirm: state => {
state.isConfirmed = true;
state.isOpened = false;
},
cancel: state => {
state.isCancelled = true;
state.isOpened = false;
},
},
});
export const confirmationModalActions = confirmationModalSlice.actions;
export default confirmationModalSlice;
This is the thunk action for it:
import { createAsyncThunk } from '#reduxjs/toolkit';
import ThunkApiConfig from '../../../types/ThunkApiConfig';
import { AppState } from '../../reducers';
import { confirmationModalActions } from './slice';
const confirmationModalThunkActions = {
open: createAsyncThunk<boolean, void, ThunkApiConfig>(
'controls/confirmationModal',
async (_, { extra, dispatch }) => {
const store = extra.store;
dispatch(confirmationModalActions.open());
return await new Promise<boolean>(resolve => {
store.subscribe(() => {
const state: AppState = store.getState();
if (state.controls.confirmationModal.isConfirmed) {
resolve(true);
}
if (state.controls.confirmationModal.isCancelled) {
resolve(false);
}
});
});
},
),
};
export default confirmationModalThunkActions;
You can notice it uses extra.store to perform the subscribe. We need to provide it when creating a store:
import combinedReducers from './reducers';
import { configureStore, getDefaultMiddleware } from '#reduxjs/toolkit';
import { ThunkExtraArguments } from '../types/ThunkExtraArguments';
function createStore() {
const thunkExtraArguments = {} as ThunkExtraArguments;
const customizedMiddleware = getDefaultMiddleware({
thunk: {
extraArgument: thunkExtraArguments,
},
});
const store = configureStore({
reducer: combinedReducers,
middleware: customizedMiddleware,
});
thunkExtraArguments.store = store;
return store;
}
export default createStore();
Now, let's create a hook that allows us to dispatch all of the above actions:
import { useDispatch, useSelector } from 'react-redux';
import { AppState } from '../../../reducers';
import { useCallback } from 'react';
import confirmationModalThunkActions from '../thunk';
import { confirmationModalActions } from '../slice';
import { AppDispatch } from '../../../../index';
export function useConfirmationModalState() {
const dispatch: AppDispatch = useDispatch();
const { isOpened } = useSelector((state: AppState) => ({
isOpened: state.controls.confirmationModal.isOpened,
}));
const open = useCallback(() => {
return dispatch(confirmationModalThunkActions.open());
}, [dispatch]);
const confirm = useCallback(() => {
dispatch(confirmationModalActions.confirm());
}, [dispatch]);
const cancel = useCallback(() => {
dispatch(confirmationModalActions.cancel());
}, [dispatch]);
return {
open,
confirm,
cancel,
isOpened,
};
}
(don't forget to attach confirm and cancel to the buttons in your modal)
And that's it! We can now dispatch our confirmation modal:
export function usePostControls() {
const { deleteCurrentPost } = usePostsManagement();
const { open } = useConfirmationModalState();
const handleDelete = async () => {
const { payload: isConfirmed } = await open();
if (isConfirmed) {
deleteCurrentPost();
}
};
return {
handleDelete,
};
}
Please, check the Edit
I'm trying to implement sagas in my app.
Right now I am fetching the props in a really bad way.
My app consists mainly on polling data from other sources.
Currently, this is how my app works:
I have containers which have mapStateToProps, mapDispatchToProps.
const mapStateToProps = state => {
return {
someState: state.someReducer.someReducerAction,
};
};
const mapDispatchToProps = (dispatch) => {
return bindActionCreators({someAction, someOtherAction, ...}, dispatch)
};
const something = drizzleConnect(something, mapStateToProps, mapDispatchToProps);
export default something;
and then, I have actions, like this:
import * as someConstants from '../constants/someConstants';
export const someFunc = (someVal) => (dispatch) => {
someVal.methods.someMethod().call().then(res => {
dispatch({
type: someConstants.FETCH_SOMETHING,
payload: res
})
})
}
and reducers, like the below one:
export default function someReducer(state = INITIAL_STATE, action) {
switch (action.type) {
case types.FETCH_SOMETHING:
return ({
...state,
someVar: action.payload
});
I combine the reducers with redux's combineReducers and export them as a single reducer, which, then, I import to my store.
Because I use drizzle, my rootSaga is this:
import { all, fork } from 'redux-saga/effects'
import { drizzleSagas } from 'drizzle'
export default function* root() {
yield all(
drizzleSagas.map(saga => fork(saga)),
)
}
So, now, when I want to update the props, inside the componentWillReceiveProps of the component, I do:
this.props.someAction()
Okay, it works, but I know that this is not the proper way. Basically, it's the worst thing I could do.
So, now, what I think I should do:
Create distinct sagas, which then I'll import inside the rootSaga file. These sagas will poll the sources every some predefined time and update the props if it is needed.
But my issue is how these sagas should be written.
Is it possible that you can give me an example, based on the actions, reducers and containers that I mentioned above?
Edit:
I managed to follow apachuilo's directions.
So far, I made these adjustments:
The actions are like this:
export const someFunc = (payload, callback) => ({
type: someConstants.FETCH_SOMETHING_REQUEST,
payload,
callback
})
and the reducers, like this:
export default function IdentityReducer(state = INITIAL_STATE, {type, payload}) {
switch (type) {
case types.FETCH_SOMETHING_SUCCESS:
return ({
...state,
something: payload,
});
...
I also created someSagas:
...variousImports
import * as apis from '../apis/someApi'
function* someHandler({ payload }) {
const response = yield call(apis.someFunc, payload)
response.data
? yield put({ type: types.FETCH_SOMETHING_SUCCESS, payload: response.data })
: yield put({ type: types.FETCH_SOMETHING_FAILURE })
}
export const someSaga = [
takeLatest(
types.FETCH_SOMETHING_REQUEST,
someHandler
)
]
and then, updated the rootSaga:
import { someSaga } from './sagas/someSagas'
const otherSagas = [
...someSaga,
]
export default function* root() {
yield all([
drizzleSagas.map(saga => fork(saga)),
otherSagas
])
}
Also, the api is the following:
export const someFunc = (payload) => {
payload.someFetching.then(res => {
return {data: res}
}) //returns 'data' of undefined but just "return {data: 'something'} returns that 'something'
So, I'd like to update my questions:
My APIs are depended to the store's state. As you may understood,
I'm building a dApp. So, Drizzle (a middleware that I use in order
to access the blockchain), needs to be initiated before I call
the APIs and return information to the components. Thus,
a. Trying reading the state with getState(), returns me empty contracts
(contracts that are not "ready" yet) - so I can't fetch the info - I
do not like reading the state from the store, but...
b. Passing the state through the component (this.props.someFunc(someState), returns me Cannot read property 'data' of undefined The funny thing is that I can console.log the
state (it seems okay) and by trying to just `return {data:
'someData'}, the props are receiving the data.
Should I run this.props.someFunc() on, for e.g., componentWillMount()? Is this the proper way to update the props?
Sorry for the very long post, but I wanted to be accurate.
Edit for 1b: Uhh, so many edits :)
I solved the issue with the undefined resolve. Just had to write the API like this:
export function someFunc(payload) {
return payload.someFetching.then(res => {
return ({ data: res })
})
}
I don't want to impose the pattern I use, but I've used it with success for awhile in several applications (feedback from anyone greatly appreciated). Best to read around and experiment to find what works best for you and your projects.
Here is a useful article I read when coming up with my solution. There was another, and if I can find it -- I'll add it here.
https://medium.com/#TomasEhrlich/redux-saga-factories-and-decorators-8dd9ce074923
This is the basic setup I use for projects.
Please note my use of a saga util file. I do provide an example of usage without it though. You may find yourself creating something along the way to help you reducing this boilerplate. (maybe even something to help handle your polling scenario).
I hate boilerplate so much. I even created a tool I use with my golang APIs to auto-generate some of this boilerplate by walking the swagger doc/router endpoints.
Edit: Added container example.
example component
import React, { Component } from 'react'
import { connect } from 'react-redux'
import { bindActionCreators } from 'redux'
import { getResource } from '../actions/resource'
const mapDispatchToProps = dispatch =>
bindActionCreators(
{
getResource
},
dispatch
)
class Example extends Component {
handleLoad = () => {
this.props.getResource({
id: 1234
})
}
render() {
return <button onClick={this.handleLoad}>Load</button>
}
}
export default connect(
null,
mapDispatchToProps
)(Example)
example action/resource.js
import { useDispatch } from 'react-redux'
const noop = () => {}
const empty = []
export const GET_RESOURCE_REQUEST = 'GET_RESOURCE_REQUEST'
export const getResource = (payload, callback) => ({
type: GET_RESOURCE_REQUEST,
payload,
callback,
})
// I use this for projects with hooks!
export const useGetResouceAction = (callback = noop, deps = empty) => {
const dispatch = useDispatch()
return useCallback(
payload =>
dispatch({ type: GET_RESOURCE_REQUEST, payload, callback }),
// eslint-disable-next-line react-hooks/exhaustive-deps
[dispatch, ...deps]
)
}
Fairly basic redux action file.
example reducers/resource.js
export const GET_RESOURCE_SUCCESS = 'GET_RESOURCE_SUCCESS'
const initialState = {
resouce: null
}
export default (state = initialState, { type, payload }) => {
switch (type) {
case GET_RESOURCE_SUCCESS: {
return {
...state,
resouce: payload.Data,
}
}
}
Fairly standard reducer pattern - NOTE the use of _SUCCESS instead of _REQUEST here. That's important.
example saga/resouce.js
import { takeLatest } from 'redux-saga/effects'
import { GET_RESOUCE_REQUEST } from '../actions/resource'
// need if not using the util
import { GET_RESOURCE_SUCCESS } from '../reducers/resource'
import * as resouceAPI from '../api/resource'
import { composeHandlers } from './sagaHandlers'
// without the util
function* getResourceHandler({ payload }) {
const response = yield call(resouceAPI.getResouce, payload);
response.data
? yield put({ type: GET_RESOURCE_SUCCESS, payload: response.data })
: yield put({
type: "GET_RESOURCE_FAILURE"
});
}
export const resourceSaga = [
// Example that uses my util
takeLatest(
GET_RESOUCE_REQUEST,
composeHandlers({
apiCall: resouceAPI.getResouce
})
),
// Example without util
takeLatest(
GET_RESOUCE_REQUEST,
getResourceHandler
)
]
Example saga file for some resource. This is where I wire up the api call with the reducer call in array per endpoint for the reosurce. This then gets spread over the root saga. Sometimes you may want to use takeEvery instead of takeLatest -- all depends on the use case.
example saga/index.js
import { all } from 'redux-saga/effects'
import { resourceSaga } from './resource'
export const sagas = [
...resourceSaga,
]
export default function* rootSaga() {
yield all(sagas)
}
Simple root saga, looks a bit like a root reducer.
util saga/sagaHandlers.js
export function* apiRequestStart(action, apiFunction) {
const { payload } = action
let success = true
let response = {}
try {
response = yield call(apiFunction, payload)
} catch (e) {
response = e.response
success = false
}
// Error response
// Edit this to fit your needs
if (typeof response === 'undefined') {
success = false
}
return {
action,
success,
response,
}
}
export function* apiRequestEnd({ action, success, response }) {
const { type } = action
const matches = /(.*)_(REQUEST)/.exec(type)
const [, requestName] = matches
if (success) {
yield put({ type: `${requestName}_SUCCESS`, payload: response })
} else {
yield put({ type: `${requestName}_FAILURE` })
}
return {
action,
success,
response,
}
}
// External to redux saga definition -- used inside components
export function* callbackHandler({ action, success, response }) {
const { callback } = action
if (typeof callback === 'function') {
yield call(callback, success, response)
}
return action
}
export function* composeHandlersHelper(
action,
{
apiCall = () => {}
} = {}
) {
const { success, response } = yield apiRequestStart(action, apiCall)
yield apiRequestEnd({ action, success, response })
// This callback handler is external to saga
yield callbackHandler({ action, success, response })
}
export function composeHandlers(config) {
return function*(action) {
yield composeHandlersHelper(action, config)
}
}
This is a very shortened version of my saga util handler. It can be a lot to digest. If you want the full version, I'll see what I can do. My full one handles stuff like auto-generating toast on api success/error and reloading certain resources upon success. Have something for handling file downloads. And another thing for handling any weird internal logic that might have to happen (rarely use this).
I am trying to make an API request using Axios in React-Redux environment. On the console everything seems to be fine, however if I try to access any of the data I either get undefined or empty array.
This is my component:
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { discoverMovie } from '../actions'
//Home component
class Home extends Component {
//make request before the render method is invoked
componentWillMount(){
this.props.discoverMovie();
}
//render
render() {
console.log('movie res ',this.props.movies.movies.res);
console.log('movie ',this.props.movies);
return (
<div>
Home
movie
</div>
)
}
};
const mapStateToProps = (state) => {
return{
movies : state.movies
}
}
export default connect(mapStateToProps, { discoverMovie })(Home);
This is my action
import { DISCOVER_MOVIE } from '../constants';
import axios from 'axios';
//fetch movie
const fetchMovie = () => {
const url = 'https://api.themoviedb.org/3/discover/movie?year=2018&primary_release_year=2018&page=1&include_video=false&include_adult=false&sort_by=vote_average.desc&language=en-US&api_key=72049b7019c79f226fad8eec6e1ee889';
let result = {
res : [],
status : ''
};
//make a get request to get the movies
axios.get(url).
then((res) => {
result.res = res.data.results;
result.status = res.status;
return result;
});
//return the result after the request
return result;
}
//main action
const discoverMovie = () =>{
const result = fetchMovie();
//return the action
return {
type : DISCOVER_MOVIE,
payload : result
}
}
export default discoverMovie;
This is the reducer
import { DISCOVER_MOVIE } from '../constants';
//initial state
const initialState = {
movies : {},
query : '',
};
//export module
export default (state = initialState, actions) =>{
switch(actions.type){
case DISCOVER_MOVIE :
return {
...state,
movies : actions.payload
};
default :
return state;
}
}
this is the log that I get from the console
as you can see if I log the entire object I see all data, however if go deep and try to access the result I either get an undefined or an empty array and using redux-dev-tools I noticed that the state does not contain any value.
I read on internet including this portal similar issue but could not find any solution for my issue.
Solution
From official docs:
You may use a dedicated status field in your actions
Basically you need to dispatch action for each state to make an async action to work properly.
const searchQuery = () => {
return dispatch => {
dispatch({
type : 'START',
})
//make a get request to get the movies
axios.get(url)
.then((res) => {
dispatch({type : 'PASS', payload : res.data});
})
.catch((err) => {
dispatch({type : 'FAILED', payload : res.error});
});
}
With redux-thunk it's pretty simple to set up. You just have to make some changes to your store. Out the box, I'm pretty sure redux isn't the most friendly with async and that's why thunk is there.
import { ..., applyMiddleware } from "redux";
import thunk from "redux-thunk";
...
const store = createStore(reducer, applyMiddleware(thunk));
...
Then in your action you'll need to return dispatch which will handle your logic for your axios call.
const fetchMovie = () => {
return dispatch => {
const url = //Your url string here;
axios.get(url).then(res => {
dispatch(discoverMovie(res.data.results, res.status);
}).catch(err => {
//handle error if you want
});
};
};
export const discoverMovie = (results, status) => {
return {
type: DISCOVER_MOVIE,
payload: results,
status: status
};
};
Your reducer looks fine, though with the way my code is typed you'll have status separately. You can combine them into it's own object before returning in discoverMovie, if you need status with the results.
This is my first answer on stack so let me know if I can clarify anything better!
I am implementing asynchronous action creators using react-redux and redux-thunk. However, I am getting the following error message: Uncaught Error: Actions must be plain objects. Use custom middleware for async actions.
I know that actions are supposed to be plain objects, and that middleware like thunk is supposed to take care of the cases when they are not. I have read several tutorials and looked at every SO question I could find on this, but I still can't figure out where I'm going wrong. Am I setting up thunk incorrectly, or am I using action creators in a bad way? Might this be an error with webpack or something?
Below I've included the code snippets I believe are relevant. Please let me know if additional info is needed.
index.js
import React from 'react';
import ReactDOM from 'react-dom';
import { Route } from 'react-router';
import { Provider } from 'react-redux';
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import rootReducer from './reducers';
import Layout from './components/Layout.js';
import OrderPage from './containers/order-page';
const store = createStore(
rootReducer,
applyMiddleware(thunk)
);
ReactDOM.render(
<Provider store={store}>
<App>
<Route exact path="/" component={Layout}/>
</App>
</Provider>,
document.querySelector('.app'));
reducers/order.js
import { FETCH_ERR, FETCH_SUCCESS, START_FETCH } from '../actions/types';
const initialState = {
fetching: false
};
export default (state=initialState, action)=>{
switch (action.type) {
case START_FETCH:
return {
fetching: true
}
case FETCH_ERR:
return {
err: action.payload.err,
fetching: false
}
case FETCH_SUCCESS:
return {
price: action.payload,
fetching: false
}
default:
return state
}
}
actions/price-fetch.js
import axios from 'axios'
const FETCH_ERR = 'FETCH_ERR'
const FETCH_SUCCESS = 'FETCH_SUCCESS'
const START_FETCH = 'START_FETCH'
const fetchSucc = (data)=>{
return{
type:FETCH_SUCCESS,
payload:data
}
}
const fetchFail = (message)=>{
return{
type:FETCH_ERR,
payload:message
}
}
const startFetch = () =>{
return{
type: START_FETCH,
payload:null
}
}
const fetchPrices = () =>{
return async (dispatch) =>{
try {
dispatch(startFetch())
let data = await axios.get('mybackendurl')
dispatch(fetchSucc(data))
} catch (error) {
dispatch(fetchFail({err:'failed to get shit'}))
}
}
}
export {
FETCH_ERR,
FETCH_SUCCESS,
fetchPrices,
START_FETCH
}
Relevant pieces of containers/order.js
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { fetchPrices } from '../actions/price-fetch';
class Order extends Component {
...
render() {
this.props.fetchPrices();
return ...
}
const mapDispatchToProps = dispatch => {
return {
fetchPrice: () => {
dispatch(fetchPrices())
}
}
}
function mapStateToProps(state){
return {
prices: state.order
}
}
export default connect(mapStateToProps, mapDispatchToProps)(Order);
Thanks in advance for any help!
In case anyone comes across the same issue. The problem was not in the code shown above, or how I was dispatching actions. I had a duplicate definition of the redux-store in a different file, which overwrote the definition with the middleware.
In my case, I had the action declaration like below, due to which it was throwing such error.
export const withdrawMoney = (amount) => {
return (dispath) => {
dispath({
type: "withdraw",
payload: amount
})
}};
What I did was just changed my action definition to be an object type
export const depositMoney = (amount) => ({
type: "deposit",
payload: amount
});
And it jsut worked fine!
If anyone is here grasping at straws when using ImmutableJS + Typescript, turns out that you HAVE to define the "initialState" for the middleware to actually apply.
export const store = createStore(
combineReducers({APIReducer}),
{},
applyMiddleware(thunk.withExtraArgument(api))
);
I suspect it may be because you have an async (dispatch) function. That would cause it to return a Promise, which may be even confusing thunk.
In normal scenarios, the function itself would return another function, which thunk would inject the dispatch and call again and you would call dispatch inside the function:
arg => dispatch => dispatch({ type: arg });
When you add async, it basically becomes the same as this:
arg => dispatch => Promise.resolve(dispatch({ type: arg }));
You may have to ditch async/await inside of there and just use axios as a normal Promise, or add something extra to ensure it returns a nothing instead of a Promise.
const fetchPrices = () =>{`
return async (dispatch) =>{`
try {
dispatch(startFetch())
let data = await axios.get('mybackendurl')
dispatch(fetchSucc(data))
} catch (error) {
dispatch(fetchFail({err:'failed to get shit'}))
}
}
}
is returning a promise so when you do
const mapDispatchToProps = dispatch => {
return {
fetchPrice: () => {
dispatch(fetchPrices())
}
}
}
dispatch(fetchPrices()) is getting a promise not a plain object
the way i do these things is leave the heavy weight to my action; call async, when resolved dispatch data to store and in your component listen for and handle data(prices list) change.
const mapDispatchToProps = dispatch => {
return {
fetchPrice
}
}
you can thus show "loading please wait" while price list is empty and promise is not resolved/rejected