I have a sagas.js in containers/App/sagas.js which contains a login saga, I'm trying to call it from a login popup which also has its own sagas.js (components/LoginPopup/sagas.js) and tied the following solution together:
// containers/App/saga.js:
export const LOGIN = 'FUNAPP/App/LOGIN';
export const requestLogin = (payload: Object) => ({
type: LOGIN + REQUESTED,
payload,
});
export function* LoginRequest(payload) {
window.alert("LOGIN REQUEST ");
window.alert(payload)
}
export default function*(): Saga<void> {
yield [
takeLatest(LOGIN + REQUESTED, LoginRequest)
];
}
// components/LoginPopup/saga.js:
import { requestLogin, LoginRequest, LOGIN } from '../../containers/App/sagas';
export default function* (): Saga<void> {
yield [
takeLatest(LOGIN + REQUESTED, LoginRequest)
]
}
// components/LoginPopup/index.js
import { requestLogin } from '../../containers/App/sagas'
const mapStateToProps = state => ({
});
const mapDispatchToProps = dispatch => ({
requestLogin: (payload) => dispatch(requestLogin(payload))
});
export default compose(
injectSagas({ key: 'app', saga, reducer }),
connect(mapStateToProps, mapDispatchToProps)
)(LoginPopup);
This feels like a dirty way of doing things, I was hoping all I would need to do is import the requestLogin function and everything would just work, or at least something more clean than exporting all the relevant functions into the LoginPopup saga there.
As far as I understand this is what you might be looking for:
// components/LoginPopup/saga.js:
import { all, call } from 'redux-saga/effects'
import loginRquest from '../../containers/App/sagas';
export default function* (): Saga<void> {
yield all([
call(loginRquest),
])
}
window.loginRquest = require('../../containers/App/sagas');
Then you can access loginRquest as global javascript(vanilla js) variable in console.
function* sagaOne() {
}
function* sagaTwo(action) {
yield call(sagaOne);
}
This one will call sagaOne from sagaTwo. The only question is: what you want to do with it? If you want to connect it with redux actions or make it callable from other saga or whatever. Technically you can launch both sagas from same action type by using takeLatest array. If you want to have result in one saga within another the above solution is the way to go with.
Related
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).
As part of my ongoing project to learn React (I'm natively an ASP.NET guy) I've hit this issue. I have a suite of React apps in which I want to use some common UI elements, so I've attempted to break these out into a separate npm package. For the shared components themselves this has worked fine.
However, some of these components depend on redux actions to operate, so I've tried to bundle these actions and a reducer function into the external package. Here's a simplified version of my actions\index.js:
export const SNACKBAR_MESSAGE = "SNACKBAR_MESSAGE";
export const SNACKBAR_HIDE = "SNACKBAR_HIDE";
export function showSnackBarMessage(message) {
console.log('hit 1');
return (dispatch, getState) => {
console.log('hit 2');
dispatch(hideSnackBar());
dispatch({
type: SNACKBAR_MESSAGE,
message: message
});
}
}
export const hideSnackBar = () => {
type: SNACKBAR_HIDE
};
And this is reducer\index.js:
import {
SNACKBAR_MESSAGE,
SNACKBAR_HIDE
} from "../actions";
const initialState = {
snackBarMessage: null,
snackBarVisible: false
};
export default function UiReducer(state = initialState, action) {
switch(action.type) {
case SNACKBAR_MESSAGE:
return Object.assign({}, state, {
snackBarMessage: action.message,
snackBarVisible: true
});
case SNACKBAR_HIDE:
return Object.assign({}, state, {
snackBarMessages: '',
snackBarVisible: false
});
default:
return state;
}
}
This is the same code that worked fine when part of the original project. These are exported by my package's entry point file like this:
// Reducer
export { default as uiReducer } from './reducer';
// Actions
export { showSnackBarMessage as uiShowPrompt } from './actions';
export { hideSnackBar as uiHidePrompt } from './actions';
Then in my consuming project, my default reducer looks like this:
import { routerReducer } from 'react-router-redux';
import { combineReducers } from 'redux';
import { uiReducer } from 'my-custom-ui-package';
// Import local reducers
const reducer = combineReducers(
{
// Some local reducers
ui: uiReducer
}
);
export default reducer;
The problem is when I try to dispatch one of these actions imported from my external package. I include the action, e.g. import { uiShowPrompt } from "my-custom-ui-package"; and dispatch it like dispatch(uiShowPrompt("Show me snackbar")); then I see the two console messages (hit 1 and hit 2) displayed, but then the following error:
Uncaught TypeError: Cannot read property 'type' of undefined
at store.js:12
at dispatch (applyMiddleware.js:35)
at my-custom-ui-package.js:1
at index.js:8
at middleware.js:22
at store.js:15
at dispatch (applyMiddleware.js:35)
at auth.js:28
at index.js:8
at middleware.js:22
The store itself looks like this:
import { createStore, combineReducers, applyMiddleware, compose } from "redux";
import thunk from 'redux-thunk';
import { browserHistory } from "react-router";
import {
syncHistoryWithStore,
routerReducer,
routerMiddleware
} from "react-router-redux";
import reducer from "./reducer";
const loggerMiddleware = store => next => action => {
console.log("Action type:", action.type);
console.log("Action payload:", action.payload);
console.log("State before:", store.getState());
next(action);
console.log("State after:", store.getState());
};
const initialState = {};
const createStoreWithMiddleware = compose(
applyMiddleware(
loggerMiddleware,
routerMiddleware(browserHistory),
thunk)
)(createStore);
const store = createStoreWithMiddleware(reducer, initialState);
export default store;
I'm afraid I don't understand this error. I don't see what I'm doing differently other than essentially moving identical code from my local project to an npm package. Since neither the actions nor reducer actually depend on redux, my npm package doesn't itself have a dependency on react-redux. Is that a problem? If there's anything else I could share to help you help me just let me know. Like I say, I'm still fairly new to all this so clearly there's something I'm not getting right!
The problem might be in declaration of hideSnackBar function
export const hideSnackBar = () => {
type: SNACKBAR_HIDE
};
Here the function is trying to return an Object Literal from Arrow Function. This will always return undefined. As the parser doesn't interpret the two braces as an object literal, but as a block statement. Thus the error, Cannot read property 'type' of undefined as store is expecting an action with property type.
Replace code like this and see if it works.
export const hideSnackBar = () => ({
type: SNACKBAR_HIDE
});
The parentheses forces it to parse as Object Literal. Hope this helps
I had exported it like
export default userReducer();
and not like this:
export default userReducer;
Just get rid of that ()
Found out that it was case of wrong order in receiving the arguments when using redux-thunk.
// wrong argument order
const anAction = () => (getState, dispatch) => {...}
// correct one
const anAction = () => (dispatch, getState) => {...}
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
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.
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