Redux: Are only synchronous calls allowed from reducer functions? - javascript

I have a reactJs app and right now I'm learning Redux to use it as Flux implementation.
I've created a store and I have created my first reducer function but now I have some questions that come to my mind, please help me to understand.
As you can see I have an action called 'FIND_PRODUCTS' which is basically fetching data from a backend service. To call this backend service I use basically an asynchronous ajax call, so basically the problem I'm facing is that the state is returned from the reducer function before my backend call has finished, so then the state is not updated correctly and the subscribers to the store are getting incorrect data. This problem is solved if I switch to a synchronous call, but then, the first warning I get is that synchronous call should be avoided because it might decrease the user's experience (performance).
So my question, can we only fetch data synchronously from a reducer function?
Should the fetching data happens in the reducer function or there is another way to do that? if so, what is it?
Does this model of redux of having a single object tree to maintain the state scales well with large applications? If I have 1000 actions the switch in my reducer function will be huge! How can we avoid that?
Thank you!!
const initialState = {
availableLocales: [{text: 'En'}, {text: 'Es'}, {text: 'Fr'}],
selectedLocale: 'En',
translations: i18n.getTranslations(),
products: []
};
const reducer = (state = initialState, action = {type: 'NONE'})=> {
//To make the reducer a pure function
deepFreeze(state);
deepFreeze(action);
switch (action.type) {
case 'SWITCH_LOCALE':
let newState = Object.assign({}, state, {
selectedLocale: action.locale,
translations: i18n.getTranslations(action.locale)
});
return newState;
case 'FIND_PRODUCTS':
let newState = Object.assign({}, state, {
products:ProductHelper().findProductsByProductType(action.productType)
});
return newState;
default:
return state
}
return state;
}
// Create a Redux store holding the state of your app.
// Its API is { subscribe, dispatch, getState }.
const store = createStore(reducer);
// You can subscribe to the updates manually, or use bindings to your view layer.
store.subscribe(() =>
console.log(store.getState())
);
export default store;

Consider this:
Create actions.js file and export the actions functions like this:
import * as types from '../constants/action_types';
import * as api from '../utils/api'
export function something1(someId){
return (dispatch) => {
dispatch({type: `${types.SOMETHING1}_PENDING`});
api.getSomething(someId)
.then((res) => {
dispatch({
type: `${types.SOMETHING1}_SUCCEEDED`,
somethings: res.body
});
.catch((err) => {
dispatch({
type: `${types.SOMETHING1}_FAILED`,
errors: err.body
})
});
}
}
export function something2(someOtherId){
return (dispatch) => {
dispatch({type: `${types.SOMETHING2}_PENDING`});
api.getSomething2(someOtherId)
.then((res) => {
dispatch({
type: `${types.SOMETHING2}_SUCCEEDED`,
otherThings: res.body
});
.catch((err) => {
dispatch({
type: `${types.SOMETHING2}_FAILED`,
errors: err.body
})
});
}
}
Then the state only change when you have the data
Next separate your reducers in separate files and create one file to export them all
like this reducers/index.js:
export { default as reducer1 } from './reducer1';
export { default as reducer2 } from './reducer2';
export { default as reducer3 } from './reducer3';
export { default as reducer4 } from './reducer4';
Then config your store like this:
configure_store.js
import { createStore, combineReducers, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import * as reducers from '../reducers';
const rootReducer = combineReducers(reducers);
const createStoreWithMiddleware = applyMiddleware(thunk)(createStore);
export default function configureStore(initialState) {
return createStoreWithMiddleware(rootReducer, initialState);
}
Finally add this to your root:
import configureStore from '../store/configure_store';
const store = configureStore();
class Root extends Component {
render() {
return (
...
<Provider store={ store } >
...
</Provider>
);
}
}
export default Root;

First, you CAN'T fetch data in reducer, because it needs to be pure by redux definition. You should create action creator, that would fetch data asynchronously and pass it to reducer. Actions CAN be impure.
Here you can read more http://redux.js.org/docs/advanced/AsyncActions.html
Also you can use middleware like redux-thunk to simplify this. https://github.com/gaearon/redux-thunk
As for the second question, you can have more than one reducer in your app. and than combine them with combineReducers(...) function http://redux.js.org/docs/basics/Reducers.html

As redux documentation said, reducers should be pure functions, so it shouldn't do ajax requests.
Better way to do so is use redux-thunk middleware, that allows you to call dispatch several times in one action.
So, in your example you do something like this:
// definition of action creator
function loadProducts(productType) {
return {type: 'FIND_PRODUCTS', productType: productType}
}
...
// calling dispatch of your action
dispatch(loadProducts(productType));
But with redux-thunk your action creator will be something like this:
function loadProducts(productType) {
return function(dispatch){
dispatch({type: 'FIND_PRODUCT_STARTED'});
// I don'h know how findProductsByProductType works, but I assume it returns Promise
ProductHelper().findProductsByProductType(productType).then(function(products){
dispatch({type: 'FIND_PRODUCT_DONE', products: products});
});
}
}
And your reducer will become pure function:
...
case 'FIND_PRODUCTS_DONE':
let newState = Object.assign({}, state, {
products: action.products,
});
return newState;
...
In this case you can also handle loading state, i.e. set loading flag in your state to true when action.type is FIND_PRODUCT_STARTED.
In my example I assume that findProductsByProductType returns Promise. In this case you can even use redux-promise-middleware without redux-thunk, it will do all work for you:
function loadProducts(productType) {
return {
type: 'FIND_PRODUCT',
payload: {
promise: ProductHelper().findProductsByProductType(productType)
}
}
}

You should not use ProductHelper() in your reducer to request data.
Instead, you should use an action creator to dispatch an action that requests the data from your API. Your API middleware would return a promise that on completion would dispatch an action intent with payload for your reducer to consume and for it to return the next state.
I recommend you look at Redux Thunk and Redux API middleware

Related

Using axios with React and Redux

I want to get data from an API and put them into my documents, and I know how to do that, but I don't know the right way. I've seen in some tutorials saying that we have to do every API calls in middlewares, but I can't find any example.
My code:
listReducer.js
const initialState = {
documents: []
}
export default function documents(state = initialState, action) {
switch (action.type) {
case 'LOAD_DOCS':
//set documents = response from api
default:
return state
}
}
listAction.js
export function loadDocs(offset, range) {
return {
type: 'LOAD_DOCS',
offset,
range
}
}
store.js
import { createStore } from 'redux'
import reducers from './reducers'
const store = createStore(reducers)
export default store
Hi You can create a thunk using a redux middleware "Redux-thunk".
thunk is basically a function in which you can make an API call and after getting the data from the server you can dispatch an action to set the data into the reducer.
You Get the dispatch function in the thunk u will create.
So instead of call the dispatch directly you will call the thunk function

Actions must be plain objects while using redux-thunk

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

Using React, why is redux saga not intercepting the action?

I'm very new to redux-saga and am trying to get a simple demo working that makes an API call and sends the response data to a reducer so it can be saved to the store. It is my understanding that the redux-saga flow should work as follows.
A component calls an action creator
The action creator then emits an action using a specific type
The watcher sagas all listen for any actions emitted and intercept an action that it is listening for. It then calls the appropriate worker saga.
The worker saga makes an API call and dispatches an action to the reducers with the type of action and the payload.
The reducer listens for any dispatched actions and if it matches, it then uses the supplied data to update the state in the store.
I have laid out my code to follow that flow but things aren't working quite right. Let me show my code and then i'll elaborate on the problem.
components/PostsIndex.js
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { fetchPosts } from '../actions';
class PostsIndex extends Component {
componentDidMount() {
this.props.fetchPosts();
}
renderPosts() {
console.log(this.props.posts);
}
render() {
return (
<div>
{this.renderPosts()}
</div>
);
}
}
const mapStateToProps = state => ({
posts: state.posts
});
export default connect(mapStateToProps, { fetchPosts })(PostsIndex);
actions/index.js
import axios from 'axios';
export const FETCH_POSTS = 'FETCH_POSTS';
export const fetchPosts = () => {
console.log('fetchPosts() in actions');
return {
type: FETCH_POSTS
};
};
sagas/index.js
import 'regenerator-runtime/runtime';
import { call, put, takeEvery, takeLatest } from 'redux-saga/effects';
import axios from 'axios';
import { FETCH_POSTS } from '../actions';
const ROOT_URL = 'https://reduxblog.herokuapp.com/api';
const API_KEY = '?key=asdsd1234';
// Watcher sagas
// Listen for an action and run the appropriate Worker saga
export function* watchFetchPosts() {
yield takeEvery(FETCH_POSTS, workFetchPosts);
}
// Worker sagas
// Respond to the actions that are caught by the watcher sagas
export function* workFetchPosts() {
try {
console.log('workFetchPosts() in sagas');
// Try to call the API
console.log('Attempting to call the posts API');
const uri = `${ROOT_URL}/posts${API_KEY}`;
const response = yield call(axios.get, uri);
console.log('Fetched Posts Response in saga worker: ', response);
yield put({
type: FETCH_POSTS,
payload: response
});
} catch (error) {
// Act on the error
console.log('Request failed! Could not fetch posts.');
console.log(error);
}
}
// Root sagas
// Single entry point to start all sagas at once
export default function* rootSaga() {
console.log('redux saga is running');
yield [watchFetchPosts()];
}
reducers/PostsReducer.js
import { mapKeys } from 'lodash';
import { FETCH_POSTS } from '../actions';
export default (state = {}, action) => {
switch (action.type) {
case FETCH_POSTS:
console.log(action);
// Create a new state object that uses an AJAX request response and grabs the 'id' property from each object in the response to use as its key
return mapKeys(action.payload.data, 'id');
}
return state;
};
It seems like the reducers are still picking up the emitted actions, which is wrong. I did notice that if I run the AJAX call in the action creator as well, then the saga will run, but the saga should intercept the communication between the action creator and the reducer, so something isn't set up quite right. Any ideas?
A full environment of my workflow can be edited at https://stackblitz.com/edit/react-redux-saga-demo. It might be easier to see the problem there.
Sagas do not stop actions from reaching the reducers. The saga middleware explicitly does the equivalent of:
next(action); // pass the action onwards to the reducers
processSagas(action);
So, the reducers will always see an action first, and the saga behavior will be excecuted after that.
The other issue is that it looks like you're trying to use the same action type to trigger the fetch behavior in the saga, and process the results in the reducer. I find that if you're using sagas, you generally have some actions that are "signals" meant to trigger saga behavior, and others that are intended to actually be handled by the reducers and update state. So, in your case, I would suggest using "FETCH_POSTS" as the signal to kick off the fetching, and have the saga then dispatch "FETCH_POSTS_SUCCESS" once the data is received and have the reducer respond to that action instead. (And, after noticing that you had that StackBlitz example up, I confirmed that just dispatching the results as "FETCH_POSTS_SUCCESS" does indeed work as I'd expect it too.)

Difficulties when try to map the Redux state with the props of the container

I am trying to get familiar with the flow of the react-boilerplate.
Till now I love how neat clean and easy to understand are things, I although feel that I miss a piece of the puzzle. Would be nice if someone with more experience could help me with that.
The problem I am facing at the moment goes as follows.
I am triggering an action within componentWillMount() of a specific component.
The action is being created in actions.js, its a simple get request made with axios.
The data are being processed in a promise middleware library redux-promise.
The promise is now being passed into the reducer of the specific component, where the whole state and the data that I need are being returned.
Trying to catch this state at the component is where I fail. I am trying to mapStateToProps but cannot find the data that I need there instead a Map {} is being received.
How do I Map this object with my props ?
I am sure I miss something important.
Here is my repo.
https://github.com/paschalidi/blog-react-redux
And here is my code so you can have a brief look.
index.js
import React from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux'
import { fetchPosts } from './actions'
import selectPostsIndex from './selectors'
export class PostsIndex extends React.Component { // eslint-disable-line react/prefer-stateless-function
componentWillMount() {
this.props.fetchPosts();
}
render() {
return (
<div>
<h3>Posts</h3>
<ul className="list-group">
A list would render here.
</ul>
</div>
);
}
}
function mapStateToProps(state) {
console.log(state.posts)
//return { posts: state } //****I dont get why the redux state is not being given here.
}
function mapDispatchToProps(dispatch) {
return bindActionCreators({ fetchPosts }, dispatch);
}
export default connect(mapStateToProps, mapDispatchToProps)(PostsIndex);
actions.js
import axios from 'axios'
import { FETCH_POSTS } from './constants';
const ROOT_URL = 'http://reduxblog.herokuapp.com/api';
const API_KEY = '?key=dsklhfksdhfjkdshfkjdshkj';
export function fetchPosts() {
const request = axios.get(`${ROOT_URL}/posts${API_KEY}`);
return {
type: FETCH_POSTS,
payload: request
};
}
store.js
import promise from 'redux-promise';
const middlewares = [
sagaMiddleware,
routerMiddleware(history),
promise
];
reducer.js
import { fromJS } from 'immutable';
import {
FETCH_POSTS
} from './constants';
const initialState = fromJS({ all:[], post: null });
function postsIndexReducer(state = initialState, action) {
switch (action.type) {
case FETCH_POSTS:
return { ...state, all: action.payload.data };
default:
return state;
}
}
export default postsIndexReducer;
Also the action is being registered in reducers.js
import PostsReducer from 'containers/PostsIndex/reducer'
export default function createReducer(asyncReducers) {
return combineReducers({
route: routeReducer,
language: languageProviderReducer,
posts: PostsReducer,
form: reduxFormReducer,
...asyncReducers,
});
}
Note I didn't test your code, but it looks like your reducer puts the fetched data in the field all of your global states posts field, but your mapStateToProps doesn't pick that up. Note that mapStateToProps should slice the part of the global state that the given component is interested in.
After a successful fetch the state you receive in mapStateToProps should look something like this:
{
posts: {
all: // whatever fetch returned
post: null
}
}
So your mapStateToProps could look something like this (note that this method receives the global state as an argument, not just for the specific reducer):
function mapStateToProps(state) {
// in component this.props.posts is { all: /* fetch result */, post: null }
return { posts: state.posts }
}
Also try to debug these methods, it becomes clearer once you see the flow of the data!
This GitHub issue covers this exact problem: https://github.com/reactjs/react-redux/issues/60.
I had to manually extract the values from the Map in mapStateToProps function:
const mapStateToProps = (state) => {
return {
posts: state.get('posts'),
};
}
Thanks to this StackOverflow post.

React / Redux: mapStateToProps not actually mapping state to props

I'm using React and Redux on a project, and I'm having problems implementing a feature to enable/disable a button. I've been able to:
trigger a method
have that method trigger an action creator
dispatch an action
catch that action in the reducer and create a new, updated state
see the updated state in Redux DevTools
However, the enable/disable functionality still doesn't work, as it seems that mapStateToProps and connect aren't actually mapping the state to the props. I'm tracking canSubmit, which changes within the state but is undefined in the props. What am I missing to successfully map the state to the props?
Relevant code:
UserFormView.js
const mapStateToProps = (state) => ({
routerState: state.router,
canSubmit: state.canSubmit
});
const mapDispatchToProps = (dispatch) => ({
actions: bindActionCreators(ActionCreators, dispatch)
});
class UserFormView extends React.Component {
...
}
export default connect(mapStateToProps, mapDispatchToProps)(UserFormView);
Actions:
export function enableSubmit(payload) {
return {
type: ENABLE_SUBMIT,
payload: payload
};
}
export function disableSubmit(payload) {
return {
type: DISABLE_SUBMIT,
payload: payload
};
}
Reducer (using a createReducer helper function):
const initialState = {
canSubmit: false
};
export default createReducer(initialState, {
[ENABLE_SUBMIT]: (state) => {
console.log('enabling');
return Object.assign({}, state, {
canSubmit: true
});
},
[DISABLE_SUBMIT]: (state) => {
console.log('disabling');
return Object.assign({}, state, {
canSubmit: false
});
}
});
Seems like you're not creating reducer with key canSubmit. It depends on your store configuration, to be more specific — on how you import your default export from reduces file. Another thing to mention here, it's likely you'll have reducer with the name canSubmit and a key canSubmit, so you'll need to reference it in code like state.canSubmit.canSubmit — you're returning object from action handlers on reducer, not simple true or false boolean values.

Categories

Resources