Related
This is my first react project and first JS project overall so I am incredibly new to the language though I have some prior experience with c#.
I got my app to a working version locally but my states and prop passing was getting out of hand so I wanted to implement redux before it gets worse.
I have a getClients Axios function that seems to work with a local state but something about the way its set up isn't working for redux.
I set up console logs and debuggers and it looks like the reducer is seeing the action.payload just fine and should be updating the state. but my clientList variable with useSelector is always showing an empty array.
export default function ClientDisplay() {
// const [clients, setClients] = useState([]);
const clientList = useSelector((state) => state.clientList);
const dispatch = useDispatch();
useEffect(() => {
getClients();
console.log("initial load");
}, []);
const getClients = () => {
Axios.get("http://localhost:3001/table/clients").then((response) => {
console.log("getClients trigered")
dispatch(setClientList(response.data));
});
};
import { createSlice } from "#reduxjs/toolkit"
export const clientListSlice = createSlice({
name: 'ClientList',
initialState: [],
reducers: {
setClientList(state, action) {
state = action.payload;
},
},
});
export const {setClientList} = clientListSlice.actions
export default clientListSlice.reducer;
const store = configureStore({
reducer : {
activeClient: activeClientReducer,
clientList: clientListReducer,
},
})
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<Provider store={store}>
<App />
</Provider>
);
Try defining the initialState properly.
export const clientListSlice = createSlice({
name: 'clientList',
initialState: {clients:[]},
reducers: {
setClientList(state, action) {
state.clients = action.payload;
},
},
})
and then in useSelector
const {clients} = useSelector((state) => state.clientList);
Newbie to Redux here, I have tried to follow a couple tutorials and I am not clear of how Redux actually works. It was mentioned that the store of Redux is to store the state of the whole tree. I have created and used actions, reducers, and store for my program and it works.
The question is, how do I retrieve what is in the store? Lets say after updating my component, how can I retrieve the value inside the component and to post it?
How can I know what changed in my dropdown list and to retrieve it?
Full code in Sandbox here https://codesandbox.io/s/elated-goldberg-1pogb
store.js
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import rootReducer from './RootReducer';
export default function configureStore() {
return createStore(
rootReducer,
applyMiddleware(thunk)
);
}
ProductsList.js
import React from "react";
import { connect } from "react-redux";
import { fetchProducts } from "./SimpleActions";
class ProductList extends React.Component {
constructor(props)
{
super(props);
this.state = {
selecteditems: '',
unitPrice: 0
}
}
componentDidMount() {
this.props.dispatch(fetchProducts());
}
componentDidUpdate(prevProps, prevState) {
if(prevState.selecteditems !== this.state.selecteditems)
{
this.setState((state, props) => ({
unitPrice: ((state.selecteditems * 1).toFixed(2))
}));
}
}
render() {
const { error, loading, products } = this.props;
if (error) {
return <div>Error! {error.message}</div>;
}
if (loading) {
return <div>Loading...</div>;
}
return (
<div>
<select
name="sel"
className="sel"
value={this.state.selecteditems}
onChange={(e) =>
this.setState({selecteditems: e.target.value})}
>
{products.map(item =>
<option key={item.productID} value={item.unitPrice}>
{item.itemName}
</option>
)}
</select>
<p>Unit Price: RM {this.state.unitPrice} </p>
</div>
);
}
}
const mapStateToProps = state => {
const products = state.productsReducer.items;
const loading = state.productsReducer.loading;
const error = state.productsReducer.error;
return {
products,
loading,
error,
}
};
export default connect(mapStateToProps)(ProductList);
SimpleAction.js
export function fetchProducts() {
return dispatch => {
dispatch(fetchProductsBegin());
return fetch('http://localhost:55959/api/products')
.then(handleErrors)
.then(res => res.json())
.then(results => {
dispatch(fetchProductsSuccess(results));
return results;
})
.catch(error => dispatch(fetchProductsFailure(error)));
};
}
function handleErrors(response) {
if(!response.ok) {
throw Error (response.statusText);
}
return response;
}
export const FETCHPRODUCTS_BEGIN = 'FETCHPRODUCTS_BEGIN';
export const FETCHPRODUCTS_SUCCESS = 'FETCHPRODUCTS_SUCCESS';
export const FETCHPRODUCTS_FAILURE = 'FETCHPRODCUTS_FAILURE';
export const fetchProductsBegin = () => ({
type: FETCHPRODUCTS_BEGIN
});
export const fetchProductsSuccess = products => ({
type: FETCHPRODUCTS_SUCCESS,
payload: {products}
});
export const fetchProductsFailure = error => ({
type: FETCHPRODUCTS_FAILURE,
payload: {error}
});
Thanks in advance!
You will need to pass your action handlers to connect function
connect(mapStateToProps,{actions})(ProductList).
how do I retrieve what is in the store? Lets say after updating my component, how can I retrieve the value inside the component and to post it?
if you want to see how is store change, you can add redux-logger to middleware to see that. when store change, it's likely a props change, you can handle this in function componentDidUpdate.
How can I know what changed in my dropdown list and to retrieve it?
values in dropdown is controlled by "const products = state.productsReducer.items;", productsReducer is controlled by actions you passed in dispatch like this: "this.props.dispatch(fetchProducts());".
I think you should add redux-logger to know more how to redux work, it show on console step by step. It will help you learn faster than you think :D
to retrieve it you forgot the selecteditems
const mapStateToProps = state => {
const products = state.productsReducer.items;
const loading = state.productsReducer.loading;
const error = state.productsReducer.error;
const selecteditems = state.prodcuts.selecteditems;
return {
products,
loading,
error,
selecteditems
};
};
To change it you should connect another function like
const mapDispatchToProps = dispatch => {
return {
onChangeDropdownSelection: (selected)=> dispatch(actions.setSelectedDropdown(selected))
}
}
Solution(updated):
I thought any action would cause react-redux-connect to call the mapState functions but when an action doesn't change anything then this is not the case.
I have a localStorage module that dispatches actions but don't change state, instead thy will write to localStorage. The module has selectors that are used in the containers but they won't get called until the state actually changes so the UI would only show correctly after another action was dispatched that would change the state.
Problem
When I put the store on window (window.store=store), add a console.log in the mapStateToProps, then in the console I dispatch an action: store.dispatch({type:'some action'}) then the console.log of the mapStateToProps does not show.
I do memoize the result but the mapStateToProps should be called see here
Full code is here and running example here (you can open a console clicking on 'console' link in the right bottom of the screen).
package.json
store.js:
import { createStore } from 'redux';
export default (initialState, reducer) => {
const store = createStore(
reducer,
initialState,
window.__REDUX_DEVTOOLS_EXTENSION__ &&
window.__REDUX_DEVTOOLS_EXTENSION__()
);
window.store = store;
return store;
};
app.js
import React from 'react';
import { connect } from 'react-redux';
import './App.css';
import createStore from './store';
import { Provider } from 'react-redux';
import initCounter from './components/Counter';
import {
createWrapper,
memoize,
} from './components/#common';
const COUNTER = 'COUNTER';
const selectCounterState = state => state.myCounter;
const counter = initCounter({
actionWrapper: createWrapper(COUNTER, 'counter1'),
selectors: { myState: selectCounterState },
connect,
memoize,
});
const initialState = {
myCounter: counter.initialState,
};
const reducer = (state = initialState, action) => {
if (action.emittedBy === COUNTER) {
return {
...state,
myCounter: counter.reducer(
selectCounterState(state),
action.payload
),
};
}
return state;
};
const store = createStore(initialState, reducer);
const Counter = counter.container;
const App = () => (
<Provider store={store}>
<Counter id="counter1" parentId={[]} />
</Provider>
);
export default App;
component/Counter/index:
import component from './component';
const INCREASE = 'INCREASE';
const reducer = (state, action) => {
if (action.type === INCREASE) {
return { ...state, count: state.count + 1 };
}
return state;
};
const makeState = memoize =>
memoize((id, parentId, { count }) => ({
id: parentId.concat(id),
parentId,
count,
}));
const mapStateToProps = ({ myState }, memoize) => () => {
const newState = makeState(memoize);
return (state, ownProps) =>
console.log('in map state to props', new Date()) ||
newState(
ownProps.id,
ownProps.parentId,
myState(state)
);
};
export default ({
actionWrapper,
selectors,
connect,
memoize,
}) => {
const actions = {
increase: ({ id }) =>
actionWrapper({
type: INCREASE,
id,
}),
};
const container = connect(
mapStateToProps(selectors, memoize),
actions
)(component);
return {
container,
reducer,
initialState: { count: 0 },
};
};
components/counter/component.js:
import React from 'react';
export default props => (
<div>
<button onClick={() => props.increase(props)}>
add
</button>
{props.count}
</div>
);
This problem was caused because I had a localStorage module that did dispatch actions but did not change the state, instead it would write to localStorage.
The module had selectors that would get the right data and the containers would use them to build the correct state but since the dispatched action did not change the state in the redux store react-redux would skip calling my mapState functions (probably memoizing state in Provider).
The solution is to let the root reducer return a new state reference {...state} so any action would cause the mapState functions to be called.
Your example codepen works just fine, you just have to trigger an action that gets past your top level guard and is of the expected structure, as to not cause any followup errors:
Post this into the console of your codepen:
store.dispatch({emittedBy: "COUNTER", type: "COUNTER -> INCREASE", id: "counter1", payload: {type: "INCREASE", id: ["counter1"]}})
I am trying to determine how to pull in multiple pieces of data to use in the same component.
Every example I see with React/Redux requests very specific data and has reducers and actions to handle that exact type of data. However, I have not been able to find information about handling more generic data.
For example, I have a few different components (or categories) on my site. One of those components is Cards. So, if a user clicks on the link for /cards/hockey it should request the hockey data from the API (if it isn't in the store already), and display it in the Cards page. If a user clicks the link for /cards/football, it should follow the same procedure, checking to see if it has the data in store yet, if not pulling it from the API, and displaying the Cards page with that data.
Another component type might be stats with stats about different sports teams.
I will not always know what types of cards are available ahead of time, so I cannot hardcode the specific sports types in my application.
So in this case, I'd like to only create two components: cards and stats, but have dynamically loaded data to populate those components.
Right now I have too much repetition going on and it is hard coded. This means that I cannot dynamically add new types in the future without creating new code to handle each of these types.
So, for example, right now I have /actions/footballCardActions.js and /actions/hockeyCardActions.js. I then have /reducers/footballCardReducers.js and /reducers/hockeyCardReducers.js. I might have similar components for the Stats component as well.
I'm also specifying status such as FETCH_HOCKEY_CARDS_SUCCESS or FETCH_FOOTBALL_CARDS_SUCCESS.
Again these are all hard coded, which makes scalability difficult.
One example I am trying to follow is https://scotch.io/tutorials/bookshop-with-react-redux-ii-async-requests-with-thunks - but again it uses very specific data requests, rather than generic ones.
What can I do to make my code work more generically so that I do not need to hard code specific datasets. Are there any good tutorials out there that deal with a similar situation?
More clarification
One of my components (screens) is a sports card screen. The menu system (with links) is automatically generated on site load from an API so I do not always know what links are available. So, there may be links for hockey, football, as well as a number of other sports that I have not thought of. When the menu link is clicked, it will call the API for that sport type and display the data on the sports card screen.
Based on the above link (and other similar sites) I've figured out how to hard-code each request for a specific sport in the actions and reducers section, but I have not been able to figure out how to do this generically if I do not know the sports ahead of time.
Further clarification based on current answers
If someone adds a new sport to the API database called MuffiBall, my application needs to be able to handle it. So, I cannot be expected to add new JavaScript code for each new sport that is added to the API.
All sports cards retrieved from the database follow the same structure.
An outline of my current code
index.js
//index.js
//Other imports here (not shown)
import Cards from './components/CardsPage'
import * as cardActions from './actions/cardActions';
import * as statsActions from './actions/statsActions';
import configureStore from './store/configureStore';
const store = configureStore();
/* Bad place to put these, and currently I am expected to know what every sport is*/
store.dispatch(hockeyActions.fetchHockey());
store.dispatch(footballActions.fetchFootball());
store.dispatch(muffiballActions.fetchMuffiball());
render(
<Provider store={store}>
<Router>
<div>
/* Navigation menu here (not shown) */
/* Currently it is manually coded, */
/* but I will be automatically generating it based on API */
<Route exact path="/" component={Home} />
<Route path="/about" component={About} />
<Route path="/cards/:val" component={Cards} />
<Route path="/stats/:val" component={Stats} />
</div>
</Router>
</Provider>,
document.getElementById('app')
);
store/configureStore.js
// store/configureStore.js
import {createStore, compose, applyMiddleware} from 'redux';
// Import thunk middleware
import thunk from 'redux-thunk';
import rootReducer from '../reducers';
export default function configureStore(initialState) {
return createStore(rootReducer, initialState,
// Apply to store
applyMiddleware(thunk)
);
}
actions/actionTypes
// actions/actionTypes
export const FETCH_HOCKEY_SUCCESS = 'FETCH_HOCKEY_SUCCESS';
export const FETCH_FOOTBALL_SUCCESS = 'FETCH_FOOTBALL_SUCCESS';
export const FETCH_MUFFIBALL_SUCCESS = 'FETCH_MUFFIBALL_SUCCESS';
actions/hockeyActions.js (one such file for every sport - need to make this one generic file):
// hockeyActions.js (one such file for every sport - need to make this one generic file):
import Axios from 'axios';
const apiUrl = '/api/hockey/';
// Sync Action
export const fetchHockeySuccess = (hockey) => {
return {
type: 'FETCH_HOCKEY_SUCCESS',
hockey
}
};
//Async Action
export const fetchHockey = () => {
// Returns a dispatcher function
// that dispatches an action at a later time
return (dispatch) => {
// Returns a promise
return Axios.get(apiUrl)
.then(response => {
// Dispatch another action
// to consume data
dispatch(fetchHockeySuccess(response.data))
})
.catch(error => {
console.log(error)
throw(error);
});
};
};
reducers/hockeyReducers.js (one such file for every sport - need to make this one generic file)
// reducers/hockeyReducers.js (one such file for every sport - need to make this one generic file)
import * as actionTypes from '../actions/actionTypes'
export const hockeyReducer = (state = [], action) => {
switch (action.type) {
case actionTypes.FETCH_HOCKEY_SUCCESS:
return action.hockey;
default:
return state;
}
};
reducers/index.js
// reducers/index.js
import { combineReducers } from 'redux';
import {hockeyReducer} from './hockeyReducers'
import {footballReducer} from './footballReducers'
import {muffiballReducer} from './muffiballReducers'
export default combineReducers({
hockey: hockeyReducer,
football: footballReducer,
muffiball: muffiballReducer,
// More reducers for each sport here
});
components/CardsPage.js:
//components/CardsPage.js
import React from 'react';
import { connect } from 'react-redux';
class Cards extends React.Component{
constructor(props){
super(props);
this.state = {
data: this.props.data,
}
}
componentWillReceiveProps(nextProps){
this.setState({
data: nextProps.data,
})
}
render(){
return(
{/* cards displayed from this.state.data */}
)
}
}
const mapStateToProps = (state, ownProps) => {
return {
data: state[ownProps.match.params.val]
}
};
export default connect(mapStateToProps)(Cards);
take a step back and identify the data types that have unique shapes, eg cards and stats. You will build a store slice for each of these with it's own actions, reducers, and selectors. The sport should just be a variable you use as an argument to your actions and selectors.
eg
Async Action
export const fetchCards = (sport) => {
return (dispatch) => {
return Axios.get(`/api/${sport}/`)
.then(response =>
dispatch(fetchCardSuccess({ sport, data: response.data }))
)
.catch(error => {
console.log(error)
throw(error);
});
};
};
Reducer
export const cardReducer = (state = {}, action) => {
switch (action.type) {
case actionTypes.FETCH_CARD_SUCCESS:
return { ...state, [action.sport]: action.data };
default:
return state;
}
};
Card Selector
export const getSport(state, sport) {
return state.cards[sport];
}
You'll probably want another slice for managing a list of the available sports, fetched from the server, and other global data.
Soo this assumes your "generic data" always will have the same shape.
You could have a generic <Results /> component. Not sure how you are doing routing, but you can use the path name of the URL to determine which data to fetch and display.
The route component (React Router 4) could look like this:
<Route path="/cards/:id" render={props => <Results {...props} />}
Then in your <Results/> component you can use react-redux to map your redux state to the component props. In componentDidMount you could see if you have the appropriate data. If you do not have the appropriate data then dispatch an action from componentDidMount to fetch it. Something like this
import { connect } from 'react-redux';
import React from 'react';
import { fetchDataAction } from './actions';
class Results extends React.Component {
componentDidMount() {
// check if results exists, if not then fire off an action to get
// data. Use whatever async redux pattern you want
if (!this.props.results) {
this.props.fetchData();
}
}
render() { /* DO SOMETHING WITH RESULTS, OR LACK OF */ }
}
const mapStateToProps = (state, ownProps) => ({
results: state.results[ownProps.match.params.id],
});
const mapDispatchToProps = (dispatch, ownProps) => ({
fetchData() {
// send path parameter via action to kick off async fetch
dispatch(fetchDataAction(ownProps.match.params.id));
},
});
export default connect(mapStateToProps, mapDispatchToProps)(Results);
You could have a results reducer that would just be an object that maps category to results. Here is what the results reducer could look like:
export default (state = {}, action) => {
switch(action.type) {
case 'FETCH_LOADED':
const { payload: { type, results } } = action;
return {
...state,
[type]: results,
};
default:
return state;
};
};
A methodology that is picking up popularity for reusable redux actions/reducers is Redux Ducks. Here's a good helper library and example to implement this in your codebase.
Building off the example in the above link that would look something like this for you:
// remoteObjDuck.js
import Duck from 'extensible-duck'
import axios from 'axios'
export default function createDuck({ namespace, store, path, initialState={} }) {
return new Duck({
namespace, store,
consts: { statuses: [ 'NEW', 'LOADING', 'READY', 'SAVING', 'SAVED' ] },
types: [
'UPDATE',
'FETCH', 'FETCH_PENDING', 'FETCH_FULFILLED',
'POST', 'POST_PENDING', 'POST_FULFILLED',
],
reducer: (state, action, { types, statuses, initialState }) => {
switch(action.type) {
case types.UPDATE:
return { ...state, obj: { ...state.obj, ...action.payload } }
case types.FETCH_PENDING:
return { ...state, status: statuses.LOADING }
case types.FETCH_FULFILLED:
return { ...state, obj: action.payload.data, status: statuses.READY }
case types.POST_PENDING:
case types.PATCH_PENDING:
return { ...state, status: statuses.SAVING }
case types.POST_FULFILLED:
case types.PATCH_FULFILLED:
return { ...state, status: statuses.SAVED }
default:
return state
}
},
creators: ({ types }) => ({
update: (fields) => ({ type: types.UPDATE, payload: fields }),
get: (id) => ({ type: types.FETCH, payload: axios.get(`${path}/${id}`),
post: () => ({ type: types.POST, payload: axios.post(path, obj) }),
patch: () => ({ type: types.PATCH, payload: axios.patch(`${path}/${id}`, obj) })
}),
initialState: ({ statuses }) => ({ obj: initialState || {}, status: statuses.NEW, entities: [] })
})
}
and each sport would create a single duck that will reuse the same functionality.
Hockey:
// hockeyDuck.js
import createDuck from './remoteObjDuck'
export default createDuck({ namespace: 'my-app', store: 'hockeyCards', path: '/cards/hockey' })
Football:
// footballDuck.js
import createDuck from './remoteObjDuck'
export default createDuck({ namespace: 'my-app', store: 'footballCards', path: '/cards/football' })
Then combine the reducers in the store:
// reducers.js
import { combineReducers } from 'redux'
import footballDuck from './footballDuck'
import hockeyDuck from './hockeyDuck'
export default combineReducers({ [footballDuck.store]: footballDuck.reducer, [hockeyDuck.store]: hockeyDuck.reducer })
If you want to dynamically add reducers to redux on the fly you will have to use something like: https://github.com/ioof-holdings/redux-dynamic-reducer. Then you can create the duck on the fly depending on your API call response:
//get from API
var sport = "football";
var footballDuck = createDuck({ namespace: 'my-app', store: 'cards', path: `/cards/${sport}` });
store.attachReducer({ [footballDuck.store]: footballDuck.reducer });
// structure (something like...)
/*
./components
./redux
./redux/actions
./redux/reducers
./redux/sagas
./redux/types
./util
*/
/* ------------------------------------------------- */
/* package.json */
{
(...)
"proxy": "http://localhost:3000",
(...)
}
/* ------------------------------------------------- */
/* index.js or otherComponent.js */
import React from 'react'
import { render } from 'react-dom'
import { createStore, applyMiddleware } from 'redux'
import { Provider } from 'react-redux'
import reducers from './redux/reducers/index'
import logger from 'redux-logger'
import createSagaMiddleware from 'redux-saga'
import indexSagas from './redux/sagas/indexSagas'
import { environment } from './util/baseUrl'
const sagaMiddleware = createSagaMiddleware()
const store =
environment === 'DEV' ?
createStore(
reducers,
window.__REDUX_DEVTOOLS_EXTENSION__ &&
window.__REDUX_DEVTOOLS_EXTENSION__(),
applyMiddleware(sagaMiddleware, logger)
) :
createStore(
reducers,
applyMiddleware(sagaMiddleware)
)
sagaMiddleware.run(indexSagas)
render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('app'))
/* ------------------------------------------------- */
/* baseURL.js */
const DEV = 'DEV'
const PROD = 'PROD'
/*-----------------------------------------*/
/*------*/ export const environment = DEV /* <------- */
/*-----------------------------------------*/
export const baseURL =
environment === DEV ?
'/api/v1/' :
'https://abcde.website.net/api/v1/'
/* ------------------------------------------------- */
/* genericTypes.js */
export const GET_REGISTERS_REQUEST = 'GET_REGISTERS_REQUEST'
export const GET_REGISTERS_SUCCESS = 'GET_REGISTERS_SUCCESS'
export const GENERIC_ERROR_MSG = 'GENERIC_ERROR_MSG'
/* ------------------------------------------------- */
/* actions.js */
export const getRegistersRequest = ( route ) => {
return {
type: GET_REGISTERS_REQUEST,
route,
}
}
export const getRegistersSuccess = ( data ) => {
return {
type: GET_REGISTERS_SUCCESS,
data,
}
}
export const genericErrorMsg = ( errorMsg ) => {
return {
type: GENERIC_ERROR_MSG,
errorMsg,
}
}
/* ------------------------------------------------- */
/* genericReducer.js */
import { GET_REGISTERS_REQUEST, GET_REGISTERS_SUCCESS, GENERIC_ERROR_MSG } from '../types/genericTypes'
const INITIAL_STATE = {
data: [],
isFetching: false,
isLoaded: false,
error: false,
errorMsg: '',
}
const genericReducer = (state = INITIAL_STATE, action) => {
switch(action.type){
case GET_REGISTERS_REQUEST:
return {
...state,
data: [],
isFetching: true,
isLoaded: false,
error: false,
errorMsg: '',
}
case GET_REGISTERS_SUCCESS:
return {
...state,
data: action.data,
isFetching: false,
isLoaded: true,
}
case GENERIC_ERROR_MSG:
return {
...state,
isFetching: false,
error: true,
errorMsg: action.errorMsg,
}
default:
return state
}
}
export default genericReducer
/* ------------------------------------------------- */
/* yourComponent.js */
import React, { Component } from "react"
import { connect } from 'react-redux'
import { getRegistersRequest } from '../../redux/actions'
//(...)
// this.props.getRegistersRequest('cards/hockey')
// this.props.getRegistersRequest('cards/football')
//(...)
const mapStateToProps = (state) => {
return {
data: state.genericReducer.data,
isFetching: state.genericReducer.isFetching,
isLoaded: state.genericReducer.isLoaded,
error: state.genericReducer.error,
errorMsg: state.genericReducer.errorMsg,
}
}
const mapDispatchToProps = (dispatch) => {
return {
getRegistersRequest: ( route ) => dispatch(getRegistersRequest( route )),
}
}
export default connect(mapStateToProps, mapDispatchToProps)(yourComponent)
/* ------------------------------------------------- */
/* indexSagas.js */
import { takeLatest } from 'redux-saga/effects'
import axios from 'axios'
import { GET_REGISTERS_REQUEST } from '../types/genericTypes'
import { getRegistersRequest } from './genericSagas'
function* indexSagas() {
try {
yield (takeLatest(GET_REGISTERS_REQUEST, getRegistersRequest, axios))
}
catch (e) {
// (...)
}
}
export default indexSagas
/* ------------------------------------------------- */
/* genericSagas.js */
import { put } from 'redux-saga/effects'
import { getRegistersSuccess, genericErrorMsg } from '../actions'
export function* getRegistrosRequest(axios, action) {
const rest = createRest(axios)
try {
let route = ''
switch (action.route) {
case 'cards/hockey':
case 'cards/football':
route = action.route
break
default: {
yield put(genericErrorMsg('Route [ ' + action.route + ' ] not implemented yet!'))
return
}
}
const data = yield rest.get(route)
yield put(getRegistersSuccess(data))
}
catch (e) {
yield put(genericErrorMsg(e))
}
}
/* ------------------------------------------------- */
/* createRest */
import { baseURL } from '../../util/baseUrl'
function createRest(axios){
const token = localStorage.getItem('yourToken')
const rest = axios.create({
baseURL: baseURL,
headers:{
Authorization: 'Bearer ' + token
}
})
return rest
}
export default createRest
/* ------------------------------------------------- */
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