How to use two different slices with Redux-toolkit? - javascript

I'm trying to fetch data from two different API's using Redux-toolkit, however I don't want them to be fetched simultaneously. Let's say I have two buttons and if I click on the button 1 the app should fetch data from the first api and if the click is on the button 2 the data should come from the second API.
Other thing is that the API's have different structures, so I need two different slices (or reducers). The issue is, since I'm using the same store for both reducers, both API's are being fetched.
import { configureStore, ThunkAction, Action } from '#reduxjs/toolkit'
import footballReducer from 'features/tournaments/footballSlice'
import volleyballReducer from 'features/tournaments/tournamentsSlice'
export const store = configureStore({
reducer: {
matchesFootball: footballReducer, // USED TO FETCH API 1
matchesVolleyball: volleyballReducer, // USED TO FETCH API 2
}
})
export type AppDispatch = typeof store.dispatch
export type RootState = ReturnType<typeof store.getState>
export type AppThunk<ReturnType = void> = ThunkAction<
ReturnType,
RootState,
unknown,
Action<string>
>
Is there a way where I can control which reducer will be executed?
My first thoughts were:
1- Use two different slices, one for each API and execute its respective reducer (I couldn't be sure if this last part is possible)
2- To create two stores, what would make it hard to manage, since I have only two reducers for now, but it'll increase to almost 10;
3- Use only one slice, where I would set one extra reducer for each API data, in that case I believe I would have to create one different function for each fetch;
Is there a builtin way to do that? Or at least a more straightforward way, which wouldn't look like some bad trick?
import { createAsyncThunk, createSlice, PayloadAction } from "#reduxjs/toolkit";
import { RootState } from "store/store";
import http from "services/services";
import IVolleyballModel from "models/VoleyballModel";
export interface VolleyballState {
matches: IVolleyballModel[]
status: "success" | "loading" | "failed"
rounds: number
tournamentName: string
}
const initialState: VolleyballState = {
matches: [],
status: "loading",
rounds: 0,
tournamentName: ''
};
export const fetchMatches = createAsyncThunk(
"matchesList/fetchMatches",
async (gender: number) => {
const response = await http.getSLTable(gender);
return response.data;
}
);
export const tournamentsSlice = createSlice({
name: "matchesList",
initialState,
reducers: {
setTournamentName (state, action: PayloadAction<string>) {
state.tournamentName = action.payload
}
},
extraReducers: (builder) => {
builder
.addCase(fetchMatches.pending, (state) => {
state.status = "loading";
})
.addCase(fetchMatches.fulfilled, (state, action) => {
state.status = "success";
let allMatches: any[] = [];
let rounds: number = 0;
action.payload.grupos[0].rodadas.map((round: { jogos: [] }) => {
// ... SOME LOGIC
});
state.matches = [...allMatches];
state.rounds = rounds;
})
.addCase(fetchMatches.rejected, (state) => {
state.status = "failed";
});
},
});
export const { setTournamentName } = tournamentsSlice.actions
export const getData = (state: RootState) => state.matchesVolleyball;
export default tournamentsSlice.reducer;

You can totally do 1. - an extraReducer for one asyncThunk will not trigger for another asyncThunk.
That said, you might also want to explore RTK-Query, which abstracts all that fetching, state keeping and storing logic away from you.
In both cases, I would recommend you read up on it in Chapters 5, 7 and 8 of the official Redux Essentials tutorial that walks you through the different approaches and shows benefits and drawbacks of both.

Related

Redux abstraction level in selectors with typescript

this is the default implementation of redux with typescript using the connect method
import { connect, ConnectedProps } from 'react-redux'
interface RootState {
isOn: boolean
}
const mapState = (state: RootState) => ({
isOn: state.isOn,
})
const mapDispatch = {
toggleOn: () => ({ type: 'TOGGLE_IS_ON' }),
}
const connector = connect(mapState, mapDispatch)
// The inferred type will look like:
// {isOn: boolean, toggleOn: () => void}
type PropsFromRedux = ConnectedProps<typeof connector>
I want to have reusable selectors from different components I have my selectors as functions seperately rather than in an object mapState like in the example.
So my selectors in a simplified version would be something like:
selectors.js:
const selector1 = (state) => state.attr1;
const selector2 = (state) => state.attr2;
const selector3 = (state) => state.attr3;
I want to create a function that accepts an object
eg
import {selector1,selector2} from '../../selectors';
const selectors = {
attr1:selector1,
attr3:selector3,
}
pass it through a wrapper function and then that function to generate the mapState
I have successfully done that
const mapStateToProps = (selectors: Selectors) => {
return Object.keys(selectors).reduce((acc, selectorKey) => {
acc[selectorKey] = selectors[selectorKey](state);
return acc;
}, {});
};
so I can use it like this const connector =connect(mapStateToProps(selectors),actions)
but the generated output doesn't have any types. Is this feasible?
Redux suggests to retrieve the props from type PropsFromRedux = ConnectedProps<typeof connector> but in my case only the types of actions appear
Per our docs, you should avoid using connect for any new code today. Instead, you should use the React-Redux hooks API (useSelector and useDispatch).
One of the major reasons for this is that the hooks are much easier to use with TypeScript.
See our guidelines for setting up "pre-typed" hooks for use in your app code.

How to access state of one slice in reducer of another slice using redux-toolkit

I am trying to access state of another slice in reducer of another slice using getState() method but looks like this is not allowed and hence the web application breaks.
Does anyone know whats the recommended way to access state of a slice inside reducer of another slice? I need this for my web application.
Thanks in advance for your any help
According to the Redux docs, there are 3 different approaches you could use:
Many users later want to try to share data between two reducers, but
find that combineReducers does not allow them to do so. There are
several approaches that can be used:
If a reducer needs to know data from another slice of state, the state
tree shape may need to be reorganized so that a single reducer is
handling more of the data.
You may need to write some custom functions
for handling some of these actions. This may require replacing
combineReducers with your own top-level reducer function. You can also
use a utility such as reduce-reducers to run combineReducers to handle
most actions, but also run a more specialized reducer for specific
actions that cross state slices.
Middleware with async logic such as
redux-thunk have access to the entire state through getState(). An
action creator can retrieve additional data from the state and put it
in an action, so that each reducer has enough information to update
its own state slice.
Example
Lets assume we have the following slices:
const namesSlice = createSlice({
name: "Names",
initialState: {
value: [],
name: "Names"
},
reducers: {
...
}
};
const counterSlice = createSlice({
name: "Counter",
initialState: {
value: 0,
name: "Names"
},
reducers: {
...
}
};
const reducer = combineReducers({
counter: counterReducer,
names: namesReducer
});
And we want to define an action addName which will add a name (entered by the user) into the names array, but will also append the current value of the counter state onto the name before adding it.
Option 1: Restructure Slices
This involves merging the 2 slices into a single slice, so you'd end up with something like:
const namesSlice = createSlice({
name: "NamesAndCounter",
initialState: {
value: {
names: [],
counter: 0
},
name: "NamesAndCounter"
},
...
};
which would allow you to access both names and counter in the reducer.
If you don't want to restructure your state/slices, then there are options 2 and 3:
Option 2: Use reduce-reducers
A third party library reduce-reducers can be used
Here you would define a cross slice reducer which is capable of accessing both the counter and names slices:
export const crossSliceReducer = (state, action) => {
if (action.type === "CROSS_SLICE_ACTION") {
const newName = action.payload + state.counter.value;
const namesState = state.names;
state = {
...state,
names: { ...namesState, value: [...state.names.value, newName] }
};
}
return state;
};
// Combine reducers
const reducer = combineReducers({
counter: counterReducer,
names: namesReducer
});
// Add the cross-slice reducer to the root reducer
const rootReducer = reduceReducers(reducer, crossSliceReducer);
// Create store
const store = configureStore({
reducer: rootReducer
});
Then you can dispatch the following action to invoke the reducer:
dispatch({ type: "CROSS_SLICE_ACTION", payload: name });
Note: The reduce-reducers library is no longer being maintained
Option 3: Use Thunks
Using thunks (meant for async actions like calling an API) allows you to get at the entire state. You can define a thunk that references the getState function that allows you to get at any slice in the global state:
export const addWithThunk = createAsyncThunk(
"names/addWithThunk",
async (name, { getState }) => {
return name + getState().counter.value;
}
);
Thunks are defined in the extraReducers property of the argument passed to createSlice():
extraReducers: (builder) => {
builder.addCase(addWithThunk.fulfilled, (state, action) => {
state.value.push(action.payload);
});
}
And can be invoked in the same way you'd invoke a plain action:
dispatch(addWithThunk(name));
There's a CodeSandbox demo showing options 2 and 3. When you add a name using one of the Submit buttons, it will access the counter state and append the current value of the counter to the name you input before adding the name into the names state.

How to access redux-toolkit reducer/action from a class component using react-redux connect()?

I have a redux-toolkit store at store.js.
import { configureStore } from '#reduxjs/toolkit';
import productCurrency from './stateSlices/productCurrency';
const Store = configureStore({
reducer: {
productCurrency: productCurrency,
},
})
export default Store;
The createslice() function itself is in a different file and looks like this below.
import { createSlice } from '#reduxjs/toolkit'
const initialState = {
value: '$',
}
export const productCurrency = createSlice({
name: 'productCurrency',
initialState,
reducers: {
setproductCurrency(state, newState) {
state.value = newState
},
},
})
export const { setproductCurrency } = productCurrency.actions;
export default productCurrency.reducer;
My issue is that I have a class component NavSection that needs to access the initial state and the reducer action setproductCurrency() to change the state. I am trying to use the react-redux connect() function to accomplish that.
const mapStateToProps = (state) => {
const { productCurrency } = state
return { productCurrency: productCurrency.value }
}
const mapDispatchToProps = (dispatch) => {
return {
setproductCurrency: () => dispatch(setproductCurrency()),
dispatch,
}
}
export default connect(mapStateToProps, mapDispatchToProps)(NavSection);
Now, I am able to access the state by ussing this.props.productCurrency. Yet, if I try to access the setproductCurrency() by ussing this.props.setproductCurrency()... Chrome console gives me an error that "this.props.setproductCurrency() is not a function".
Is there a way of fixing this, or am I trying to do something impossible?
UPDATE #1
I think I just moved the ball in the right direction. I changed the onClick function to be an arrow function as shown below.
onClick={() => this.props.setproductCurrency('A$')}
Now, setproductCurrency() is considered a function, but it returns a different error when I click the button...
Objects are not valid as a React child (found: object with keys {type, payload}). If you meant to render a collection of children, use an array instead.
Why would this function now return an object? It is supposed to change the state and trigger a re-render of the page so that the class component can access the newly changed state.
To be clear, RTK has nothing to do with React-Redux, connect, or mapDispatch :)
The current error of "Objects are not valid as a React child" is because your reducer is wrong. A reducer's signature is not (state, newState). It's (state, action). So, your line state.value = newStateis reallystate.value = action`, and that's assigning the entire Redux action object as a value into the state. That's definitely not correct conceptually.
Instead, you need state.value = action.payload.

Redux Thunk with Typescript

I am learning Typescript and I am trying to implement a simple React/Redux app. When I use sync actions it works fine, but the problems are with the async action. I am following the official redux tutorial.
First I declare the state for the session
export interface UserSessionState {
loggedIn: boolean;
}
Then I declare the interface for the action
interface UpdateSessionAction {
type: 'USER_LOGIN';
payload: boolean;
}
I export them with Union Types
export type UserActionTypes = UpdateSessionAction;
Then I have the actual Action
export function updateSession(loggedIn: UserSessionState) {
return {
type: 'USER_LOGIN',
payload: loggedIn,
};
}
I have a fake api call
function api() {
return Promise.resolve(true);
}
And finally the login
export const userLogin = (): ThunkAction<
void,
{},
{},
AnyAction
> => async (dispatch: ThunkDispatch<{}, {}, AnyAction>) => {
const res = await api();
dispatch(updateSession({ loggedIn: res }));
};
In the reducer I simply initialize the state
initialState: UserSessionState = {loggedIn: false}
Then I do the normal redux stuff for the reducer.
Finally in my store I call the initial action for checking the state
store.dispatch(userLogin());
I keep getting this error:
Argument of type 'ThunkAction<Promise<void>, {}, {}, AnyAction>' is not assignable to parameter of type 'AnyAction'.
Property 'type' is missing in type 'ThunkAction<Promise<void>, {}, {}, AnyAction>' but required in type 'AnyAction'.
I am missing a type but I have no idea what I do wrong.
In short:
You get this error because what returned from your userLogin() function is a ThunkAction, which is missing type
Why this is happening?
dispatch should accept parameter of type AnyAction.
AnyAction is a redux type, which extends Action (which have a mandatory property type).
This is from the current redux types file
export interface Action<T = any> {
type: T
}
/**
* An Action type which accepts any other properties.
* This is mainly for the use of the `Reducer` type.
* This is not part of `Action` itself to prevent users who are extending `Action.
*/
export interface AnyAction extends Action {
// Allows any extra properties to be defined in an action.
[extraProps: string]: any
}
How to fix it?
Use ThunkDispatch type instead of redux's standard Dispatch. The following example and more can be found on this Gist
const mapDispatchToProps = (dispatch: ThunkDispatch<MyState, void, Action>) => {
return {
onRequestClick: (arg: any) => dispatch(myAsyncAction(arg)),
};
}
Also, see this article, section Map Dispatch to Props
Not exactly answering the same thing the question was about, but I stumbled upon the same problem using Redux useDispatch() hook. And again not such a good support for the async action types.
What I eventualy did was to add an AsyncAction type and created a new hook for the typing:
// hooks.ts
export type AsyncAction = (dispatch: (action: Action) => any) => void;
export type Dispatcher = (action: AsyncAction | Action) => void
export const useAppDispatch: () => Dispatcher = useDispatch as any;
// async-actions.ts
import {AsyncAction} from "./hooks"
export const myAsyncAction: () => AsyncAction = () => (dispatch: (action:
Action) => any) => {
// async logic with dispatching goes here:
// dispatch(...)
}
//my-component.tsx
import {myAsyncAction} from "./async-actions"
import {useAppDispatch} from "./hooks"
export const MyComponent = () => {
const dispatch = useAppDispatch();
syncHandler () {
disaptch ({type: MY_SYNC_ACTION})
}
asyncHandler () {
disaptch (myAsyncAction())
}
...
}
Even though a bit Typescrip "hacky" the advantages of this approach are that you get full type check support for the sync and async (Thunk) actions, and from the client (component) point of view there is no difference - no need to use a different type system.

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