Redux - Loading initial state asynchronously - javascript

I'm trying to work out the cleanest way to load the initial state of my Redux stores when it comes from API calls.
I understand that the typical way of providing the initial state is to generate it server-side on page load, and provide it to Redux createStore() as a simple object. However, I'm writing an app that I'm planning on packaging up in Electron and so this doesn't work.
The best that I've been able to come up with so far is to fire an action immediately after creating the store that will go and request the initial state for the store - either one action that retrieves the entire initial state or a number of actions that each retrieve the initial state for one part of the store. This would then mean that my code looks like:
const store = createStore(reducer, Immutable.Map(), middleware);
store.dispatch(loadStateForA());
store.dispatch(loadStateForB());
store.dispatch(loadStateForC());
Whilst this will work, it seems a bit on the crude side and so I'm wondering if there's some better alternative that I'm missing?

I also encountered the same problem (also building an electron app). A part of my store has application settings which gets persisted on local file system and I needed to load it asynchronously on application's startup.
This is what I come up with. Being a "newbie" with React/Redux, I am very much interested in knowing the thoughts of the community on my approach and how it can be improved.
I created a method which loads the store asynchronously. This method returns a Promise which contains the store object.
export const configureStoreAsync = () => {
return new Promise((resolve) => {
const initialState = initialStoreState;//default initial store state
try {
//do some async stuff here to manipulate initial state...like read from local disk etc.
//This is again wrapped in its own Promises.
const store = createStore(rootReducer, initialState, applyMiddleware(thunk));
resolve(store);
});
} catch (error) {
//To do .... log error!
const store = createStore(rootReducer, initialState, applyMiddleware(thunk));
console.log(store.getState());
resolve(store);
}
});
};
Then in my application entry point, here's how I used it:
configureStoreAsync().then(result => {
const store = result;
return ReactDOM.render(
<Provider store={store}>
<App store={store}/>
</Provider>,
document.getElementById('Main'));
});
Like I said, this is my naive attempt at solving this problem and I am sure there must be better ways of handling this problem. I would be very much interested in knowing how this can be improved.

As far as I can tell, you have only two options (logically):
Set the initial state after the store is instantiated
Set the initial state when the store is instantiated
Option 1 must be done using an action:
The only way to change the state is to emit an action, an object
describing what happened.
— One of "Three Principles" in the docs
This is what you've tried, but you think it is crude for some reason.
The alternative is just to call createStore after your asynch request has resolved. One solution has already been posted (by #Gaurav Mantri) using a Promise object, which is a nice approach.
I would recommend against this, since you will likely have multiple modules trying to require or import your store (or store.dispatch, or store.subscribe) before it exists; they would all have to be made to expect Promises. The first method is the most Redux-y.

My app startup workflow:
Loading spinner in index.html
Ajax to check if user is logged in
On ajax end, render the Root component
Hide the loading spinner
I achieved that by:
Creating the store with a custom middleware that listens for the initial ajax end action and calls a callback once
Dispatching the initial ajax action
root.js
const store = createStore(
rootReducer,
applyMiddleware(
...,
actionCallbackOnceMiddleware(INITIAL_AJAX_END, render)
)
)
function render() {
ReactDOM.render(
<Provider store={store}>
<RootComponent/>
</Provider>,
document.getElementById('root')
)
document.getElementById('loading').dispatchEvent(new Event('hide'))
}
store.dispatch(initialAjaxAction());
middleware/actionCallbackOnce.js
export default (actionType, callback) => store => next => {
let called = false;
return action => {
next(action);
if (!called && action.type === actionType) {
called = true;
callback();
}
}
}
index.html
<div id="loading">
<span>Loading</span>
<style type="text/css">...</style>
<script>
(function(loading){
loading.addEventListener('hide', function(){
loading.remove();
});
loading.addEventListener('error', function(){
loading.querySelector('span').textContent = "Error";
});
})(document.getElementById('loading'));
</script>
</div>
<div id="root"></div>

Using extraReducers with createAsyncThunk seems to be the clean way of doing this as explained here

Using async thunks would give you more control. This approach worked for me. In this example the user has a setting for the UI's theme, and this setting will be persisted to the backend. We can't render the UI until we know this setting.
Add an Async Thunk to a Slice: Here we use createAsyncThunk. Async thunks are actions but with the additional ability to (i) perform an API request, (ii) update the state using results from API request. (I'm assuming here you are using redux slices, if you are not then just add this thunk to your main reducer).
// ./store/settings.js
import {
createAsyncThunk,
createReducer,
} from '#reduxjs/toolkit';
import { client } from './api/client';
const initialState = {
theme: 'light', // can be either 'light', 'dark' or 'system'
};
const fetchSettings = createAsyncThunk('settings/fetchSettings', async () => {
const response = await client.fetch('/api/v1/settings');
// `response` is an object returned from server like: { theme: 'dark' }
return response;
});
const settingsReducer = createReducer(initialState, builder => {
builder.addCase(fetchSettings.fulfilled, (state, action) => {
state.theme = action.payload.theme;
});
});
export { fetchSettings };
export default settingsReducer;
Combine Reducers: With slices your state is divided up and so you'll be bringing all your reducers together into one single reducer (some redux boilerplate has bene replaced with // ...):
// ./store/index.js
// ...
// import fooReducer from './store/foo';
// import barReducer from './store/bar';
import settingsReducer from './store/settings';
export const store = configureStore({
reducer: {
// foo: fooReducer,
// bar: barReducer,
settings: settingsReducer,
},
});
// ...
export const { useDispatch, useSelector }
Dispatch Thunk: Dispatching the async thunk will perform the API request and update the store. With async thunks you can use await to wait until this is all done. We won't perform the initial render until this is done.
// ./index.js
import App from './components/App';
import { store } from './store/index';
import { fetchSettings } from './store/settings';
async function main() {
await store.dispatch(fetchSettings());
root.render(
<StrictMode>
<App store={store} />
</StrictMode>,
);
}
main();
Render App: The app will use this updated store and render the theme from the backend.
// ./components/App.js
import { useSelector } from './store/index';
export default function App({ store }) {
// read theme from store
const settings = useSelector(state => state.settings);
const settingsTheme = settings.theme;
return (
<Provider store={store}>
<div>Your app goes here. The theme is ${settingsTheme}</div>
</Provider>
);
}

Related

Problem with custom ContextProvider state when using custom hook for fetching data in multiple tables in React

I have multiple tables, of small size, and I want to be able to write / read / update my components when the corresponding table has been updated by the app (we can consider it's a single user app for the moment).
I've been inspired by this question to write a custom Provider and associated hook for data fetching (and eventually posting) in my app: React useReducer async data fetch
I came up with this:
import React from "react";
import { useContext, useState, useEffect } from "react";
import axios from "axios";
const MetadataContext = React.createContext();
function MetadataContextProvider(props) {
let [metadata, setMetadata] = useState({});
async function loadMetadata(url) {
let response = await axios.get(url);
// here when I console.log the value of metadata I get {} all the time
setMetadata({ ...metadata, [url]: response.data });
}
async function postNewItem(url, payload) {
await axios.post(url, payload);
let response = await axios.get(url);
setMetadata({ ...metadata, [url]: response.data });
}
return (
<MetadataContext.Provider value={{ metadata, loadMetadata, postNewItem }}>
{props.children}
</MetadataContext.Provider>
);
}
function useMetadataTable(url) {
// this hook's goal is to allow loading data in the context provider
// when required by some component
const context = useContext(MetadataContext);
useEffect(() => {
context.loadMetadata(url);
}, []);
return [
context.metadata[url],
() => context.loadMetadata(url),
(payload) => context.postNewItem(url, payload),
];
}
function TestComponent({ url }) {
const [metadata, loadMetadata, postNewItem] = useMetadataTable(url);
// not using loadMetadata and postNewItem here
return (
<>
<p> {JSON.stringify(metadata)} </p>
</>
);
}
function App() {
return (
<MetadataContextProvider>
<TestComponent url="/api/capteur" />
<br />
<TestComponent url="/api/observation" />
</MetadataContextProvider>
);
}
export default App;
(the code should run in CRA context, both apis can be replaced with almost any API)
When I run it, a request is fired on both endpoints (/api/capteur and /api/observation), but where I'm expecting the metadata object in the MetadataContextProvider to have 2 keys: "/api/capteur" and "/api/observation", only the content of the last request made appears.
When I console.log metadata in the loadMetadata function, metadata always has the initial state hook value, that is {}.
I'm fairly new to React, I tried hard and I'm really not figuring out what's going on here. Can anyone help?
Your problem is how you update the metadata object with setMetadata.
The operation of updating the metadata object via loadMetadata in your context is done by two "instances" respectively: TestComponent #1 and TestComponent #2.
They both have access to the metadata object in your context, but they're not instantly synchronized, as useState's setter function works asynchronously.
The easy solution for your problem is called functional updates.
useState's setter does also provide a callback function, which will then use (I'm oversimplifying here) the "latest" state.
In your context provider:
async function loadMetadata(url) {
let response = await axios.get(url);
setMetadata((existingData) => ({ ...existingData, [url]: response.data }));
// instead of
// setMetadata({ ...metadata, [url]: response.data });
}
Here is a working CodeSandbox: https://codesandbox.io/s/elegant-mclean-syiol?file=/src/App.js
Look at the console to see the order of execution.
I highly recommend to fully read React hooks documentation, especially the "Hooks API Reference". There are also other problems with your code (for example missing dependencies in the useEffect hook, do you have ESLint enabled?).
If you want to have a better overview on how to use React's context I can recommend Kent C. Dodds' blog:
https://kentcdodds.com/blog/application-state-management-with-react
https://kentcdodds.com/blog/how-to-use-react-context-effectively

What is the best practices for redirecting users in React applications?

I have seen much more cases related to redirecting users in react applications and every case was just a different approach to the solution. There are some cases, where redirecting has occurred in actions like this`
export const someAction = (values, history) => async dispatch => {
const res = await someAsyncOperation(props);
history.push('/home');
dispatch(someAction);
}
In this example history object (form react-router) is being passed in react component. For me, this approach is not acceptable.
There is also a special Redirect from react-router.
After then I have already searched many articles and couldn't just find anything.
So in your opinion, what's the best practice for redirecting and where to handle such kind of processes ?
In React, you usually achieve redirects in the componentDidUpdate of your components.
In the case of async actions, you will check a flag stored in the Redux store, generally a boolean like isFetching, isCreating, isUpdating, etc…, which will be modified by the actions.
Simple example:
class EditUser extends Component {
compondentDidUpdate(prevProps) {
if (prevProps.isUpdating && !this.props.isUpdating) {
// ↑ this means that the async call is done.
history.push('/users')
}
}
updateUser() {
const modifiedUser = // ...
this.props.updateUser(modifiedUser)
// ↑ will change state.users.isUpdating from false to true during the async call,
// then from true to false once the async call is done.
}
render() {
// ...
<button onClick={this.updateUser}>Update</button>
// ...
}
}
const mapStateToProps = (state, props) => ({
userToEdit: state.users.items.find(user => user.id === props.userId)
isUpdating: state.users.isUpdating,
})
const mapActionsToProps = {
updateUser: usersActions.updateUser,
}
export default connect(mapStateToProps, mapActionsToProps)(EditUser)
The next step is usually to add another flag in your Redux store to track if the async calls are successful or not (e.g. state.users.APIError, in which you can keep the error returned by the API). Then you achieve the redirect only if there are no errors.
We mostly redirect a user due to when user logged in or when sign out. For example here's basic requireAuth HOC component to check if user is logged in or not and redirect him to another place.
import React, { Component } from 'react';
import { connect } from 'react-redux';
export default ChildComponent => {
class ComposedComponent extends Component {
componentDidMount() {
this.shouldNavigateAway();
}
componentDidUpdate() {
this.shouldNavigateAway();
}
shouldNavigateAway() {
if (!this.props.auth) {
this.props.history.push('/');
}
}
render() {
return <ChildComponent {...this.props} />;
}
}
function mapStateToProps(state) {
return { auth: state.auth.authenticated };
}
return connect(mapStateToProps)(ComposedComponent);
};
There are two position to check if user is logged in
When the first time that component mount - in componentDidMount()
When user try to sign in , log in or sign out - in componentDidUpdate()
Also in your code sample, history.push is in an action creator. Action creators belongs to redux side. Keep redux & react separate.

Store does not have a valid reducer while the reducer is empty

I am using the generator from https://github.com/stylesuxx/generator-react-webpack-redux. The development is going fine except that I have an error in browser console
warning.js:10 Store does not have a valid reducer. Make sure the argument passed to combineReducers is an object whose values are reducers.
In index.js in reduces file, the code is stated below:
import { combineReducers } from 'redux';
const reducers = {};
const combined = combineReducers(reducers);
module.exports = combined;
In index.js in stores file, the code is stated below:
import { createStore } from 'redux';
import reducers from '../reducers';
function reduxStore(initialState) {
const store = createStore(reducers, initialState,
window.devToolsExtension && window.devToolsExtension());
if (module.hot) {
// Enable Webpack hot module replacement for reducers
module.hot.accept('../reducers', () => {
// We need to require for hot reloading to work properly.
const nextReducer = require('../reducers'); // eslint-disable-line global-require
store.replaceReducer(nextReducer);
});
}
return store;
}
export default reduxStore;
I am not using redux in current development but I use the generator for configuration there for future planning. Any idea how to make the reducer empty like the code above const reducers = {}; without triggering any warning?
What you need is a valid reducer as the error suggest.
either you can try the following if you want to keep the boilerplate or
const reducers = { somename: () => {} };
const combined = combineReducers(reducers);
module.exports = combined;
or try
module.exports = () => {};
this will make sure a valid reducer function is returned. since you are not using redux this will not be a problem.
Why don't you try to create a function instead of an empty object?
According to redux documentation it should be a function:
reducer (Function): A reducing function that returns the next state tree, given the current state tree and an action to handle.
https://github.com/reactjs/redux/blob/master/docs/api/createStore.md
Also I don't know if you need to use combinereducers as you only have one.

How to properly make REST calls from ReactJS + Redux application?

I'm using ReactJS + Redux, along with Express and Webpack. There is an API built, and I want to be able to make REST calls -- GET, POST, PUT, DELETE -- from the client-side.
How and what is the properly way to go about doing so with the Redux architecture? Any good example of the flow, in terms of reducers, action creators, store, and react routes, would be extremely helpful.
Thank you in advance!
The simpliest way, is to do it using redux-thunk package. This package is an redux middleware, so first of all, you should connect it to redux:
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import rootReducer from './reducers/index';
const store = createStore(
rootReducer,
applyMiddleware(thunk)
);
This allows you to dispatch async actions along with regular sync actions. Let's create one of them:
// actions.js
export function fetchTodos() {
// Instead of plain objects, we are returning function.
return function(dispatch) {
// Dispatching REQUEST action, which tells our app, that we are started requesting todos.
dispatch({
type: 'FETCH_TODOS_REQUEST'
});
return fetch('/api/todos')
// Here, we are getting json body(in our case it will contain `todos` or `error` prop, depending on request was failed or not) from server response
// And providing `response` and `body` variables to the next chain.
.then(response => response.json().then(body => ({ response, body })))
.then(({ response, body }) => {
if (!response.ok) {
// If request was failed, dispatching FAILURE action.
dispatch({
type: 'FETCH_TODOS_FAILURE',
error: body.error
});
} else {
// When everything is ok, dispatching SUCCESS action.
dispatch({
type: 'FETCH_TODOS_SUCCESS',
todos: body.todos
});
}
});
}
}
I prefer to separate react components on presentational and container components. This approach was perfectly described in this article.
Next, we should create TodosContainer component, which would provide data to presentational Todos component. Here, we are using react-redux library:
// TodosContainer.js
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { fetchTodos } from '../actions';
class TodosContainer extends Component {
componentDidMount() {
// When container was mounted, we need to start fetching todos.
this.props.fetchTodos();
}
render() {
// In some simple cases, it is not necessary to create separate `Todos` component. You can put todos markup directly here.
return <Todos items={this.props.todos} />
}
}
// This function is used to convert redux global state to desired props.
function mapStateToProps(state) {
// `state` variable contains whole redux state.
return {
// I assume, you have `todos` state variable.
// Todos will be available in container component as `this.props.todos`
todos: state.todos
};
}
// This function is used to provide callbacks to container component.
function mapDispatchToProps(dispatch) {
return {
// This function will be available in component as `this.props.fetchTodos`
fetchTodos: function() {
dispatch(fetchTodos());
}
};
}
// We are using `connect` function to wrap our component with special component, which will provide to container all needed data.
export default connect(mapStateToProps, mapDispatchToProps)(TodosContainer);
Also, you should create todosReducer, which will handle FETCH_TODOS_SUCCESS action, and other 2 actions if you want display loader / error message.
// reducers.js
import { combineReducers } from 'redux';
const INITIAL_STATE = {
items: [],
isFetching: false,
error: undefined
};
function todosReducer(state = INITIAL_STATE, action) {
switch (action.type) {
case 'FETCH_TODOS_REQUEST':
// This time, you may want to display loader in the UI.
return Object.assign({}, state, {
isFetching: true
});
case 'FETCH_TODOS_SUCCESS':
// Adding derived todos to state
return Object.assign({}, state, {
isFetching: false,
todos: action.todos
});
case 'FETCH_TODOS_FAILURE':
// Providing error message to state, to be able display it in UI.
return Object.assign({}, state, {
isFetching: false,
error: action.error
});
default:
return state;
}
}
export default combineReducers({
todos: todosReducer
});
For other operations like CREATE, UPDATE, DELETE there is nothing special, they are implementing the same way.
The short answer is:
redux is not an architecture
You can use any library. A lot of people these days use the fetch API directly.
To be able to integrate redux with asynchronous actions (which you need for AJAX), you need to use a library to help. The most popular two are redux-thunk and redux-saga, as others have said.
For a brain-dead simple library that you can drop in to your redux app, you could try redux-crud-store. Disclaimer: I wrote it. You could also read the source for redux-crud-store if you are interested in integrating the fetch API, or another API client, with redux-saga
This is the primary use case for libraries like redux-thunk, redux-saga, and redux-observable.
redux-thunk is the simplest, where you would do something like this:
import fetch from 'isomorphic-fetch'
export const REQUEST_POSTS = 'REQUEST_POSTS'
function requestPosts(subreddit) {
return {
type: REQUEST_POSTS,
subreddit
}
}
export const RECEIVE_POSTS = 'RECEIVE_POSTS'
function receivePosts(subreddit, json) {
return {
type: RECEIVE_POSTS,
subreddit,
posts: json.data.children.map(child => child.data),
receivedAt: Date.now()
}
}
// Meet our first thunk action creator!
// Though its insides are different, you would use it just like any other action creator:
// store.dispatch(fetchPosts('reactjs'))
export function fetchPosts(subreddit) {
// Thunk middleware knows how to handle functions.
// It passes the dispatch method as an argument to the function,
// thus making it able to dispatch actions itself.
return function (dispatch) {
// First dispatch: the app state is updated to inform
// that the API call is starting.
dispatch(requestPosts(subreddit))
// The function called by the thunk middleware can return a value,
// that is passed on as the return value of the dispatch method.
// In this case, we return a promise to wait for.
// This is not required by thunk middleware, but it is convenient for us.
return fetch(`http://www.reddit.com/r/${subreddit}.json`)
.then(response => response.json())
.then(json =>
// We can dispatch many times!
// Here, we update the app state with the results of the API call.
dispatch(receivePosts(subreddit, json))
)
// In a real world app, you also want to
// catch any error in the network call.
}
}
The above example is taken directly from http://redux.js.org/docs/advanced/AsyncActions.html which is really the definitive source for answers on your question.

Accessing Redux state in an action creator?

Say I have the following:
export const SOME_ACTION = 'SOME_ACTION';
export function someAction() {
return {
type: SOME_ACTION,
}
}
And in that action creator, I want to access the global store state (all reducers). Is it better to do this:
import store from '../store';
export const SOME_ACTION = 'SOME_ACTION';
export function someAction() {
return {
type: SOME_ACTION,
items: store.getState().otherReducer.items,
}
}
or this:
export const SOME_ACTION = 'SOME_ACTION';
export function someAction() {
return (dispatch, getState) => {
const {items} = getState().otherReducer;
dispatch(anotherAction(items));
}
}
There are differing opinions on whether accessing state in action creators is a good idea:
Redux creator Dan Abramov feels that it should be limited: "The few use cases where I think it’s acceptable is for checking cached data before you make a request, or for checking whether you are authenticated (in other words, doing a conditional dispatch). I think that passing data such as state.something.items in an action creator is definitely an anti-pattern and is discouraged because it obscured the change history: if there is a bug and items are incorrect, it is hard to trace where those incorrect values come from because they are already part of the action, rather than directly computed by a reducer in response to an action. So do this with care."
Current Redux maintainer Mark Erikson says it's fine and even encouraged to use getState in thunks - that's why it exists. He discusses the pros and cons of accessing state in action creators in his blog post Idiomatic Redux: Thoughts on Thunks, Sagas, Abstraction, and Reusability.
If you find that you need this, both approaches you suggested are fine. The first approach does not require any middleware:
import store from '../store';
export const SOME_ACTION = 'SOME_ACTION';
export function someAction() {
return {
type: SOME_ACTION,
items: store.getState().otherReducer.items,
}
}
However you can see that it relies on store being a singleton exported from some module. We don’t recommend that because it makes it much harder to add server rendering to your app because in most cases on the server you’ll want to have a separate store per request. So while technically this approach works, we don’t recommend exporting a store from a module.
This is why we recommend the second approach:
export const SOME_ACTION = 'SOME_ACTION';
export function someAction() {
return (dispatch, getState) => {
const {items} = getState().otherReducer;
dispatch(anotherAction(items));
}
}
It would require you to use Redux Thunk middleware but it works fine both on the client and on the server. You can read more about Redux Thunk and why it’s necessary in this case here.
Ideally, your actions should not be “fat” and should contain as little information as possible, but you should feel free to do what works best for you in your own application. The Redux FAQ has information on splitting logic between action creators and reducers and times when it may be useful to use getState in an action creator.
When your scenario is simple you can use
import store from '../store';
export const SOME_ACTION = 'SOME_ACTION';
export function someAction() {
return {
type: SOME_ACTION,
items: store.getState().otherReducer.items,
}
}
But sometimes your action creator need to trigger multi actions
for example async request so you need
REQUEST_LOAD REQUEST_LOAD_SUCCESS REQUEST_LOAD_FAIL actions
export const [REQUEST_LOAD, REQUEST_LOAD_SUCCESS, REQUEST_LOAD_FAIL] = [`REQUEST_LOAD`
`REQUEST_LOAD_SUCCESS`
`REQUEST_LOAD_FAIL`
]
export function someAction() {
return (dispatch, getState) => {
const {
items
} = getState().otherReducer;
dispatch({
type: REQUEST_LOAD,
loading: true
});
$.ajax('url', {
success: (data) => {
dispatch({
type: REQUEST_LOAD_SUCCESS,
loading: false,
data: data
});
},
error: (error) => {
dispatch({
type: REQUEST_LOAD_FAIL,
loading: false,
error: error
});
}
})
}
}
Note: you need redux-thunk to return function in action creator
I agree with #Bloomca. Passing the value needed from the store into the dispatch function as an argument seems simpler than exporting the store. I made an example here:
import React from "react";
import {connect} from "react-redux";
import * as actions from '../actions';
class App extends React.Component {
handleClick(){
const data = this.props.someStateObject.data;
this.props.someDispatchFunction(data);
}
render(){
return (
<div>
<div onClick={ this.handleClick.bind(this)}>Click Me!</div>
</div>
);
}
}
const mapStateToProps = (state) => {
return { someStateObject: state.someStateObject };
};
const mapDispatchToProps = (dispatch) => {
return {
someDispatchFunction:(data) => { dispatch(actions.someDispatchFunction(data))},
};
}
export default connect(mapStateToProps, mapDispatchToProps)(App);
I would like to point out that it is not that bad to read from the store -- it might be just much more convenient to decide what should be done based on the store, than to pass everything to the component and then as a parameter of a function. I agree with Dan completely, that it is much better not to use store as a singletone, unless you are 100% sure that you will use only for client-side rendering (otherwise hard to trace bugs might appear).
I have created a library recently to deal with verbosity of redux, and I think it is a good idea to put everything in the middleware, so you have everyhing as a dependency injection.
So, your example will look like that:
import { createSyncTile } from 'redux-tiles';
const someTile = createSyncTile({
type: ['some', 'tile'],
fn: ({ params, selectors, getState }) => {
return {
data: params.data,
items: selectors.another.tile(getState())
};
},
});
However, as you can see, we don't really modify data here, so there is a good chance that we can just use this selector in other place to combine it somewhere else.
Presenting an alternative way of solving this. This may be better or worse than Dan's solution, depending on your application.
You can get the state from the reducers into the actions by splitting the action in 2 separate functions: first ask for the data, second act on the data. You can do that by using redux-loop.
First 'kindly ask for the data'
export const SOME_ACTION = 'SOME_ACTION';
export function someAction() {
return {
type: SOME_ACTION,
}
}
In the reducer, intercept the ask and provide the data to the second stage action by using redux-loop.
import { loop, Cmd } from 'redux-loop';
const initialState = { data: '' }
export default (state=initialState, action) => {
switch(action.type) {
case SOME_ACTION: {
return loop(state, Cmd.action(anotherAction(state.data))
}
}
}
With the data in hand, do whatever you initially wanted
export const ANOTHER_ACTION = 'ANOTHER_ACTION';
export function anotherAction(data) {
return {
type: ANOTHER_ACTION,
payload: data,
}
}
Hope this helps someone.
I know I'm late to the party here, but I came here for opinions on my own desire to use state in actions, and then formed my own, when I realized what I think is the correct behavior.
This is where a selector makes the most sense to me. Your component that issues this request should be told wether it's time to issue it through selection.
export const SOME_ACTION = 'SOME_ACTION';
export function someAction(items) {
return (dispatch) => {
dispatch(anotherAction(items));
}
}
It might feel like leaking abstractions, but your component clearly needs to send a message and the message payload should contain pertinent state. Unfortunately your question doesn't have a concrete example because we could work through a 'better model' of selectors and actions that way.
I would like to suggest yet another alternative that I find the cleanest, but it requires react-redux or something simular - also I'm using a few other fancy features along the way:
// actions.js
export const someAction = (items) => ({
type: 'SOME_ACTION',
payload: {items},
});
// Component.jsx
import {connect} from "react-redux";
const Component = ({boundSomeAction}) => (<div
onClick={boundSomeAction}
/>);
const mapState = ({otherReducer: {items}}) => ({
items,
});
const mapDispatch = (dispatch) => bindActionCreators({
someAction,
}, dispatch);
const mergeProps = (mappedState, mappedDispatches) => {
// you can only use what gets returned here, so you dont have access to `items` and
// `someAction` anymore
return {
boundSomeAction: () => mappedDispatches.someAction(mappedState.items),
}
});
export const ConnectedComponent = connect(mapState, mapDispatch, mergeProps)(Component);
// (with other mapped state or dispatches) Component.jsx
import {connect} from "react-redux";
const Component = ({boundSomeAction, otherAction, otherMappedState}) => (<div
onClick={boundSomeAction}
onSomeOtherEvent={otherAction}
>
{JSON.stringify(otherMappedState)}
</div>);
const mapState = ({otherReducer: {items}, otherMappedState}) => ({
items,
otherMappedState,
});
const mapDispatch = (dispatch) => bindActionCreators({
someAction,
otherAction,
}, dispatch);
const mergeProps = (mappedState, mappedDispatches) => {
const {items, ...remainingMappedState} = mappedState;
const {someAction, ...remainingMappedDispatch} = mappedDispatch;
// you can only use what gets returned here, so you dont have access to `items` and
// `someAction` anymore
return {
boundSomeAction: () => someAction(items),
...remainingMappedState,
...remainingMappedDispatch,
}
});
export const ConnectedComponent = connect(mapState, mapDispatch, mergeProps)(Component);
If you want to reuse this you'll have to extract the specific mapState, mapDispatch and mergeProps into functions to reuse elsewhere, but this makes dependencies perfectly clear.
I wouldn't access state in the Action Creator. I would use mapStateToProps() and import the entire state object and import a combinedReducer file (or import * from './reducers';) in the component the Action Creator is eventually going to. Then use destructuring in the component to use whatever you need from the state prop. If the Action Creator is passing the state onto a Reducer for the given TYPE, you don't need to mention state because the reducer has access to everything that is currently set in state. Your example is not updating anything. I would only use the Action Creator to pass along state from its parameters.
In the reducer do something like:
const state = this.state;
const apple = this.state.apples;
If you need to perform an action on state for the TYPE you are referencing, please do it in the reducer.
Please correct me if I'm wrong!!!

Categories

Resources