How do I call a Redux Saga action in a `onClick` event? - javascript

I just started a new project using infinitered/ignite.
I've added my getUserToken function to the APITestScreen
So I know that the function works as expected, but I'm not able to hook the method up with the onPress function to the button I added to the LaunchScreen.
I have imported it to the view, but nothing happens when I click the button. I have added an alert and a console.log, but they are not triggered. What should I do to get the fetchUserToken to run when I click the button?
The entire project posted posted at Github.
my view
import getUserToken from '../Sagas/AuthSagas.js';
<RoundedButton text="Fetch token" onPress={ getUserToken } />
App/Redux/AuthRedux.js
import { createReducer, createActions } from 'reduxsauce'
import Immutable from 'seamless-immutable'
/* ------------- Types and Action Creators ------------- */
const { Types, Creators } = createActions({
tokenRequest: ['username'],
tokenSuccess: ['token'],
tokenFailure: null
})
export const AuthTypes = Types
export default Creators
/* ------------- Initial State ------------- */
export const INITIAL_STATE = Immutable({
token: null,
fetching: null,
error: null,
username: null
})
/* ------------- Reducers ------------- */
// request the token for a user
export const request = (state, { username }) =>
state.merge({ fetching: true, username, token: null })
// successful token lookup
export const success = (state, action) => {
const { token } = action
return state.merge({ fetching: false, error: null, token })
}
// failed to get the token
export const failure = (state) =>
state.merge({ fetching: false, error: true, token: null })
/* ------------- Hookup Reducers To Types ------------- */
export const reducer = createReducer(INITIAL_STATE, {
[Types.TOKEN_REQUEST]: request,
[Types.TOKEN_SUCCESS]: success,
[Types.TOKEN_FAILURE]: failure
})
App/Sagas/AuthSagas.js
import { call, put } from 'redux-saga/effects'
import { path } from 'ramda'
import AuthActions from '../Redux/AuthRedux'
export function * getUserToken (api, action) {
console.tron.log('Hello, from getUserToken');
alert('in getUserToken');
const { username } = action
// make the call to the api
const response = yield call(api.getUser, username)
if (response.ok) {
const firstUser = path(['data', 'items'], response)[0]
const avatar = firstUser.avatar_url
// do data conversion here if needed
yield put(AuthActions.userSuccess(avatar))
} else {
yield put(AuthActions.userFailure())
}
}
Sagas/index.js
export default function * root () {
yield all([
// some sagas only receive an action
takeLatest(StartupTypes.STARTUP, startup),
// some sagas receive extra parameters in addition to an action
takeLatest(GithubTypes.USER_REQUEST, getUserAvatar, api),
// Auth sagas
takeLatest(AuthTypes.TOKEN_REQUEST, getUserToken, api)
])
}

Sagas are great because they allow long running processes to control application flow in a completely decoupled manner, and can be sequenced via actions, allowing you to parallelise/cancel/fork/reconcile sagas to orchestrate your application logic in a centralised place (ie think of it as being able to link together actions, incorporating side effects along the way)
By importing your generator function and calling it directly like a normal function won't work and if it did would be bypassing saga functionality, for example if you press a second time or third time on that button, it will always execute the entire generator again from start to finish, which as they involve async operations could result in you say trying to store or use a token that is then immediately invalidated by a subsequent saga
Better practice would be to have your saga always listening for specific actions to trigger further worker sagas, keeping them decoupled, and allowing them to control their own flow.
In this case you would dispatch an action onPress, and have a long running parent saga listening for that action which then hands off to your current one to do the actual work. This listening saga would then have control over cancelation of previous invocations using takeLatest would cancel the previous saga invocation, so that a subsequent button press while the previous was still in flight would always take precedence, and your token cannot accidentally go stale
// AuthActions.js
// add a new action (or more probably adapt fetchUserToken to suit)...
export const GET_USER_TOKEN = 'auth/get-user-token'
export const getUserToken = (username) => ({
type: GET_USER_TOKEN,
payload: username
})
// view
import {getUserToken} from './AuthActions'
// this now dispatches action (assumes username is captured elsewhere)
// also assumes store.dispatch but that would more likely be done via `connect` elsewhere
<RoundedButton text="Fetch token" onPress={ () => store.dispatch(getUserToken(this.username)) } />
// AuthSagas.js
import api from 'someapi'
import actions from 'someactions'
import {path} from 'ramda'
import {put, call, takeLatest} from 'redux-saga/effects'
import AuthActions from '../Redux/AuthRedux'
// this will be our long running saga
export function* watchRequestUserToken() {
// listens for the latest `GET_USER_TOKEN` action,
// `takeLatest` cancels any currently executing `getUserToken` so that is always up to date
yield takeLatest(AuthActions.GET_USER_TOKEN, getUserToken)
}
// child generator is orchestrated by the parent saga
// no need to export (unless for tests) as it should not be called by anything outside of the sagas
function* getUserToken (action) { // the actual action is passed in as arg
const username = action.payload
// make the call to the api
const response = yield call(api.getUser, username)
if (response.ok) {
const firstUser = path(['data', 'items'], response)[0]
const avatar = firstUser.avatar_url
// do data conversion here if needed
yield put(AuthActions.userSuccess(avatar))
} else {
yield put(AuthActions.userFailure())
}
}
// main.js (example taken from https://redux-saga.js.org/) adapted to suite
import { createStore, applyMiddleware } from 'redux'
import createSagaMiddleware from 'redux-saga'
import {reducer} from './AuthRedux'
import {watchRequestUserToken} from './AuthSagas'
// create the saga middleware
const sagaMiddleware = createSagaMiddleware()
// mount it on the Store
export const store = createStore(
reducer,
applyMiddleware(sagaMiddleware)
)
// then run the saga
sagaMiddleware.run(watchRequestUserToken)

On button you are calling fetchUserTocken but in script you define getUserToken.

Related

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.

React dispatching action does nothing

I have a problem when trying to fetch initial data for my app from an api.
My problem is that after the console.log(url) in the action, nothing happens. I see the url in the console, but the rest of the code in getInitialRuns() doesn't seem to be executed, atleast not the way I expect. I get no error messages.
When using Postman, I can succesfully get a response from that API endpoint, so the API should be fine.
I have actions that looks like this:
function requestRuns(){
console.log('request')
return {
type: 'getInitialRuns'
}
}
export function getInitialRuns(){
var url = 'http://localhost:32118/api/Runs';
console.log(url);
return dispatch => {
dispatch(requestRuns())
return fetch(url)
.then(response => response.json().then(body => ({response, body})))
.then(({response, body}) => {
if(!response.ok){
console.log('fail')
}
else{
console.log('success')
}
})
}
The component that calls the action looks like this:
class RunList extends Component{
constructor(props) {
super(props)
}
componentWillMount(){
getInitialRuns()
}
render() {
const {runs, isFetching, error} = this.props
return (
<ul className="run-list">
{runs.map((run) =>
<Run key={run.Id} id={run.Id} date={run.DateString} day={run.Day} distance={run.Distance} duration={run.DurationString} avgpace={run.AvgPaceString} calories={run.Calories}/>
)}
</ul>
)
}
}
RunList.propTypes = {
isFetching: PropTypes.bool.isRequired,
runs: PropTypes.instanceOf(Immutable.List),
error: PropTypes.object
}
function mapStateToProps(state) {
return{
runs: state.runs,
isFetching: state.isFetching,
error: state.error
}
}
export default connect(mapStateToProps)(RunList)
My store is set up like this:
import { createStore, applyMiddleware } from 'redux';
import {composeWithDevTools} from 'redux-devtools-extension';
import runs from './reducers/runs';
import thunk from 'redux-thunk';
export default createStore(runs,composeWithDevTools( applyMiddleware(thunk) ))
And these are my reducers
import Immutable from 'immutable'
let initialState = {
runs: Immutable.List([]),
isFetching: false,
error: undefined
};
export default (state = initialState, action) => {
switch(action.type) {
case 'addRun':
return state.unshift(action.run)
case 'deleteRun':
return Object.assign({}, state, {
runs: state.runs.filter((run) => run.Id !== action.id)
})
case 'getInitialRuns':
console.log('initial')
return Object.assign({}, state, {
isFetching: true
})
case 'getInitialRunsSuccess':
console.log('success')
return Object.assign({}, state, {
isFetching: false,
runs: action.runs
})
case 'getInitialRunsFailure':
return Object.assign({}, state, {
isFetching: false,
error: action.error
})
default:
return state
}
}
In order to dispatch an action on redux, you should provide a mapDispatchToProps function to connect. From redux docs:
(..) You can define a function called mapDispatchToProps() that receives the dispatch() method and returns callback props that you want to inject into the presentational component
const mapDispatchToProps = (dispatch) => {
return {
onTodoClick: (id) => {
dispatch(toggleTodo(id))
}
}
}
I see you are using some advanced libraries like Immutablejs. I suggest
you start with reading the awesome redux documentation as it will take you step by step. And until you're familiar with basic redux concepts, avoid any other library.
Here are some notes i hope are useful for you: (they are taken from redux docs)
Notes
An action object which is a payload of information that send data from your application to your store. It is of this form:
var ADD_TODO = {
type: ADD_TODO,
text: 'Build my first Redux app'
}
It's recommended to name you actions types in Upper case, like ADD_TODO.
dispatch accepts an action object (see the example above).
It is recommended to use action creators which are functions that return an action object. It makes them easily testable and portable
Action creators are usually named in lower case: addTodo().
I hope this helps a bit
You should be returning a new state if you want Redux to notice any state change.
Your reducer cases should look something like that:
return {
...previousState,
newValues
}
'addRun'
return state.unshift(action.run)
should be
return {...state, runs: state.run.unshift(action.run) }
The function getInitialRuns returns a function, so calling it doesn't execute anything of the returned function. Btw I'm not sure executing it would be of any use for your app.
Reducers are synchronous, so if you need to do anything asynchronous you would need a middleware, like redux-thunk or redux-observable.
Flow should be:
State -> Views -> Actions -> Middleware -> Actions -> Reducers -> State and back to Views
Please look at the docs at http://redux.js.org/docs/advanced/AsyncActions.html
Also you can enjoy the excellent free courses on egghead:
https://egghead.io/courses/getting-started-with-redux
https://egghead.io/courses/building-react-applications-with-idiomatic-redux
On github you'll find a lot of material on the courses too.

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.

Redux - Loading initial state asynchronously

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>
);
}

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