Redux batch doesn't work properly on React Native app - reactjs

Need to call multiple dispatches. Trying to use batch from react-redux, but it triggers only the first dispatch inside the function. Tried to exchange dispatches, the same, triggers only the first one, checked in dev tools. Thunk is connected
In app:
dispatch(
setData({
user,
userLang,
userToken,
dateMargin,
}), )
Action:
export const setData = ({user, userLang, userToken, dateMargin}) => {
return dispatch => {
batch(() => {
dispatch(setToken(userToken))
dispatch(setLang(userLang))
dispatch(setUser(user))
dispatch(setDateMargin(dateMargin))
})
}
}

Yes, batch is a re-export from react-dom. React-native does not have a similar API, so that is not possible there.
Generally, it is recommended to Model Actions as Events, Not Setters, Allow Many Reducers to Respond to the Same Action and to Avoid Dispatching Many Actions Sequentially (see the links for more explanation), so what you are doing here is pretty much an anti-pattern:
You have the logic in your component, although the big strentgh of Redux is that it moves the state logic out of your component, into your Store.
Going by the recommendations from the official Redux Style guide, you'd rather
dispatch(userLoggedIn({
userToken,
userLang,
user,
dateMargin
})
and have potentially multiple reducers acting on that event-type action.

Related

React toolkit and redux-first-router

I am digging into React with Redux for a rewrite of our product.
A lot of fog around Redux was cleared by using Redux-Toolkit https://redux-toolkit.js.org/.
Then I found that React-Router made state management messy and found a solution in redux-first-router https://github.com/faceyspacey/redux-first-router.
Now I want to combine these excellent libraries. But I think I'm doing something wrong in the configuration.
Here is the code. Starting with a sandbox example at https://codesandbox.io/s/m76zjj924j, I changed the configureStore.js file into (for simplicity I have omitted code for the user reducer)
import { connectRoutes } from 'redux-first-router';
import { configureStore, getDefaultMiddleware } from '#reduxjs/toolkit'
import { routePaths } from '../routes';
const { reducer: location } = connectRoutes(routePaths);
const {
middleware: routerMiddleware,
enhancer: routerEnhancer,
initialDispatch
} = connectRoutes(routePaths, { initialDispatch: false });
export default function configureRouteStore() {
const store = configureStore({
reducer: {
location: location
},
middleware: [...getDefaultMiddleware(), routerMiddleware],
enhancers: (defaultEnhancers) => [routerEnhancer, ...defaultEnhancers]
})
initialDispatch();
return store;
}
But now each time a change in route = Redux store is updated, I get an exception in the browser:
index.js:1 A non-serializable value was detected in the state, in the path: `location.routesMap.PROFILE.thunk`. Value: dispatch => {
dispatch(USER_DATA_LOADED({
avatar: null
}));
const avatar = `https://api.adorable.io/avatars/${Math.random()}`;
setTimeout(() => {
// fake async call
dispatch(USER_…
Take a look at the reducer(s) handling this action type: HOME.
I can see that this stems from the routes definitions if the route has a 'thunk' property defined as this: PROFILE: { path: "/profile/:username", thunk: fetchUserData },
If I change the thunk property to a serializable value (or remove it) the error is gone.
Somehow now the thunk is added to the payload of the action to update paths. What...?
What to do? OK, I can get it work with the traditional Redux setup but as I am a big fan the redux toolkit it would be sweet for me and maybe a few more people out there to make it work with the toolbox.
I'm a Redux maintainer and creator of Redux Toolkit.
Based on that error message and reading the Redux-First-Router source code, it looks like the library is indeed attempting to store thunk functions in the Redux store. This is a problem, because we specifically instruct users to never put non-serializable values like functions in state or actions.
By default, Redux Toolkit adds a "serializable state invariant middleware" that warns you if non-serializable values are detected in state or actions, to help you avoid accidentally making this mistake.
It is possible to pass some options to getDefaultMiddleware() to customize the behavior of these middlewares. There is currently an ignoredActions option, but I don't think we have an option to ignore specific sections of the state tree. The included redux-immutable-state-invariant middleware does have an ignore option for portions of the state, so perhaps we could add that approach.
I've added https://github.com/reduxjs/redux-toolkit/issues/319 to see if we can add an option like that.
In the meantime, you could potentially turn off the middleware by calling getDefaultMiddleware({serializableCheck: false}).
update
I've just published Redux Toolkit v1.2.3, which adds an ignoredPaths option to the serializability check middleware to allow ignoring specific keypaths within the state.
Again, please note that this is purely an escape hatch to work around misbehaving libraries, and should not be used as a regular approach.

React redux - how to handle events?

I am learning the concepts of Redux.
From what I understand, reducers are not supposed to mutate state, they return a new state with mutations applied. They are also not supposed to (or maybe even can't?) dispatch other actions.
Given those limitations, that pretty much leaves you with the action creators, middleware, or your React components (if using Redux with React) to call your business logic. So, what I'm trying to do is use Firebase, the wall I've hit, and the questions I have are the following:
Where should you be creating an instance (i.e. initialising Firebase)?
Similarly, where should be uninitialising Firebase? Usually I would expect these to be in a service of some kind that you'd call, but I'm not really sure where you would make the call to that service.
Where should you listen for changes? Firebase provides .on on references to allow you to react to changes (i.e. child_added, child_removed) - where should these .on handlers go? I was thinking that surely when one of these events comes in, an action needs to be dispatched with the child in question to update the state, but I can't do this in the reducers, I don't want to do this on a component, and the action creator just seems like an odd place for it to (and when I think about it, how would that even work?)!
Solution
Finally I ended up following the #ampersand suggestion. I made a module with all the listeners of Firebase application. I import in the Firebase module a dispatcher.js file, that is composed by dispatcher function like these:
import store from '../store';
import * as actions from './index';
export function addUserMessage(text) {
store.dispatch(actions.addUserMessage(text));
}
export function addResponseMessage(messageObj) {
store.dispatch(actions.addResponseMessage(messageObj));
}
What I was not understanding was how to dispatch action from an external module. Now I have the concepts clear.
I'm not familiar with Firebase, but I think middleware is most likely the answer here.
You can write a simple middleware that has access to your Firebase instance and will dispatch these message to the Redux store.
// firebase-middleware.js
export default function createFireBaseMiddleware (firebaseInstance) {
return store => dispatch => {
fireBaseinstance.on(message => {
dispatch({
type: 'FIREBASE_MESSAGE',
payload: message,
})
})
return action => {
if (action.type === 'TALK_BACK_TO_FIREBASE' {
firebaseInstance.postMessage(action)
}
// Let the action continue down the path of middleware to the store
return dispatch(action)
}
}
}
// createStore.js
export default createStore(
reducer,
applyMiddleware(createFireBaseMiddleware(myfirebaseInstance)
)
To tack onto #chautelly's response, generally services in react/redux are just modules—create your firebase instance inside a standalone module, export the appropriate object from it, and import where needed.
database.js
import firebase from 'firebase';
firebase.initializeApp({
apiKey: 'api key',
authDomain: 'db_name.firebaseio.com',
databaseURL: 'https://db_name.firebaseio.com/'
});
const database = firebase.database();
export default database;

Redux - action creators with params from different pieces of state

I'm using React Native and Redux to create an app that helps users find nearby fast-foods. I've got two components : Map and Settings.
Each component is connected (via react-redux) to its respective piece of state. Map can also dispatch an action creator called apiCall :
Map Component
...
connect(
({ map }) => ({ map }),
{ apiCall }
)(Map)
Settings Component
...
connect(
({ settings }) => ({ settings })
)(Settings)
I would like the action creator apiCall to read values from both map and settings pieces of state : this.props.apiCall(map, settings).
However, I want to avoid connecting my Map component to state.settings because it would re-render each time state.settings updates.
I'm still quite confused and have not found "the right approach" in solving this issue. These are the things I tried :
Connecting Map to state.settings and using shouldComponentUpdate() to prevent useless re-renders
Using getState() from the action creator to read state.settings value
Encapsulating everything in another higher component and then passing down specific props
The first two worked but seemed a bit anti-pattern and the third one was still triggering re-renders. Not quite sure why even though it felt like a good solution. I have not tried selectors yet but it seems to be another alternative.
To sum up, my question is :
How to dispatch an action that needs to read values from different pieces of state while avoiding unnecessary re-renders ?
You should use redux-thunk. That way you can return a function (a thunk) in your action creator, which will have access to state
const apiCall = (apiCallArgs) => {
return (dispatch, getState) => {
let mapState = getState('map');
let settingsState = getState('settings');
return dispatch(yourApiAction);
}
}

redux-sagas callback (aka sagas and setState)

I want to set form loading state (spinner icon, disable input) when the user submits the form, and clear that state when the action completes or fails. Currently I am storing this state in my store, which involves creating an action creator and reducer, which is annoying for a few reasons:
It's a lot more work than simply calling this.setState
It's more difficult to reason about
I don't really want to store local component state in my store
Essentially I want to do this within my form component:
handleFormSubmitted() {
this.setState({ isSaving: true })
this.props.dispatchSaveForm({ formData: this.props.formData })
.finally(() => this.setState({ isSaving: false }))
}
You can't do that with redux-saga. What you are trying to do there goes against the basic principles of redux-saga.
redux-saga aims to be reactive by treating actions like events that describe what's happening in your app... So that other sagas (usually "watchers" using take) or the rootReducer can subscribe to those actions/events and do what they need to do...
Because of that redux-saga -unlike redux-thunk and redux-promise-
doesn't change the behaviour of the dispatch method... So, with redux saga when you dispatch, you dispatch, and the reducers and the sagas are subscribed to the dispatched actions. But the dispatch method won't return a promise like it happens when you use other middlewares/store-enhancers.
So, the only way that redux-saga has to let the rest of the app know that the request of your form has finished is by dispatching an action (using the put effect) whenever that request finishes or errors, right? So, how could you possibly know directly from inside the component if a specific action has been dispatched?
Unless you make your own middleware (or you use a separate one) with a connector component: there is no way for you to subscribe to concrete actions inside a component.
Sure, you could access the context directly in order to get a hold of your redux store, and then you could use the redux subscribe method directly, but the listener function won't tell you what's the action that got dispatched. It will just get invoked when an action (any action) gets dispatched... maybe you could check if some property of the state has changed, but that's insane. So, unless you want to go down that route, which is crazy: you can't do that using redux-saga.
If you wanted to do something like that (which IMHO is not a very good idea) you would have to do it without using redux-saga. A possible way to do it could be something along the lines of:
handleFormSubmitted() {
this.setState({ isSaving: true })
yourFetchCall({ formData: this.props.formData })
.then(payload => this.props.dispatchFormSaved(payload))
.catch(error => this.props.dispatchSavingFormErrored(error))
.finally(() => this.setState({ isSaving: false }))
}

In React with Redux, when should I save data to back end

In React with Redux, when there are some user operations, e.g., in facebook, user adds some comments, I will call dispatch() to send the add action to redux store, but when should I call back end API to save these data to database? do I need to do it together with dispatch()?
thanks
One solution would be to transfer your API logic into a thunk using a middleware package such redux-thunk (or similar).
Using thunks allows you to treat special kinds of actions as functions which means you can extend a plain action with specific action-related logic. The example you give of needing to serialize your state is an excellent use-case for redux-thunk.
You should note that, unlike reducers, thunks explicitly support fetching state and dispatching subsequent actions via the getState and dispatch functions.
Below is an ES6 example of how such a multi-purpose thunk might look.
To demo the getState() method the new item will only be saved via the api only if the redux state shouldSave value is truthy.
I would also use the async/await syntax to make sure the the api call succeeds before dispatching the local redux action.
Thunk Example - adding a new item
import api from './api'
export const addNew = async (item) => {
return (dispatch, getState) => {
try{
const state = getState()
if(state.shouldSave){
await api.save(item)
}
dispatch({
type: ITEM_ADD_NEW,
data: item
})
}catch(err){
const error = new Error("There was a problem adding the new item")
error.inner=err
throw(error)
}
}
}

Resources