Reputation: 3278
is it possible to dispatch an action in a reducer itself? I have a progressbar and an audio element. The goal is to update the progressbar when the time gets updated in the audio element. But I don't know where to place the ontimeupdate eventhandler, or how to dispatch an action in the callback of ontimeupdate, to update the progressbar. Here is my code:
//reducer
const initialState = {
audioElement: new AudioElement('test.mp3'),
progress: 0.0
}
initialState.audioElement.audio.ontimeupdate = () => {
console.log('progress', initialState.audioElement.currentTime/initialState.audioElement.duration);
//how to dispatch 'SET_PROGRESS_VALUE' now?
};
const audio = (state=initialState, action) => {
switch(action.type){
case 'SET_PROGRESS_VALUE':
return Object.assign({}, state, {progress: action.progress});
default: return state;
}
}
export default audio;
Upvotes: 280
Views: 206687
Reputation: 535
Redux Toolkit now has a createListenerMiddleware.
A Redux middleware that lets you define "listener" entries that contain an "effect" callback with additional logic, and a way to specify when that callback should run based on dispatched actions or state changes.
It's intended to be a lightweight alternative to more widely used Redux async middleware like sagas and observables.
See https://redux-toolkit.js.org/api/createListenerMiddleware
Example of how to dispatch an action in response to a failed api call using createListenerMiddleware
from @reduxjs/toolkit
:
// Create a new file src/app/ListenerMiddleware.ts with this content.
import { createListenerMiddleware } from '@reduxjs/toolkit'
export const listenerMiddleware = createListenerMiddleware()
// Add the listenerMiddleware in src/app/store.ts
import { listenerMiddleware } from './ListenerMiddleware';
const store = configureStore({
// Existing code omitted. Add the line below.
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().prepend(listenerMiddleware.middleware),
})
// In your slice file as src/features/user/UserSlice.ts
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'
import { listenerMiddleware } from '../../app/ListenerMiddleware';
export const userSlice = createSlice({
name: 'user',
// initial state, reducers and extraReducers
})
// exports code omitted.
export const fetchSomeApi = createAsyncThunk('user/fetchSomeApi', async () => {
// Make an api call and return response data.
})
const someCodeWithSideEffects = createAsyncThunk('user/someCodeWithSideEffects', async (youCanPassData: string) => {
// Code to run if fetchSomeApi was rejected.
})
// Use listenerMiddleware to subscribe to the fetchSomeApi.rejected action and dispatch another action.
listenerMiddleware.startListening({
actionCreator: fetchSomeApi.rejected,
effect: async (action, listenerApi) => {
const payload = action.payload
await listenerApi.dispatch(someCodeWithSideEffects('some input'));
},
})
Upvotes: 3
Reputation: 7960
Since anything is technically possible, you can do it. But you SHOULD NOT do it.
Here is a quote from Dan Abramov (the creator of Redux):
"Why would you want to dispatch inside a reducer? It's grossly misusing the library. It's exactly the same as React doesn't allow you to setState inside render."
From "Forbid dispatch from inside a reducer" Github ticket that he himself created
Upvotes: 1
Reputation: 4571
Dispatching and action inside of reducer seems occurs bug.
I made a simple counter example using useReducer
which "INCREASE" is dispatched then "SUB" also does.
In the example I expected "INCREASE" is dispatched then also "SUB" does and, set cnt
to -1 and then
continue "INCREASE" action to set cnt
to 0, but it was -1 ("INCREASE" was ignored)
See this: https://codesandbox.io/s/simple-react-context-example-forked-p7po7?file=/src/index.js:144-154
let listener = () => {
console.log("test");
};
const middleware = (action) => {
console.log(action);
if (action.type === "INCREASE") {
listener();
}
};
const counterReducer = (state, action) => {
middleware(action);
switch (action.type) {
case "INCREASE":
return {
...state,
cnt: state.cnt + action.payload
};
case "SUB":
return {
...state,
cnt: state.cnt - action.payload
};
default:
return state;
}
};
const Test = () => {
const { cnt, increase, substract } = useContext(CounterContext);
useEffect(() => {
listener = substract;
});
return (
<button
onClick={() => {
increase();
}}
>
{cnt}
</button>
);
};
{type: "INCREASE", payload: 1}
{type: "SUB", payload: 1}
// expected: cnt: 0
// cnt = -1
Upvotes: 0
Reputation: 10247
Starting another dispatch before your reducer is finished is an anti-pattern, because the state you received at the beginning of your reducer will not be the current application state anymore when your reducer finishes. But scheduling another dispatch from within a reducer is NOT an anti-pattern. In fact, that is what the Elm language does, and as you know Redux is an attempt to bring the Elm architecture to JavaScript.
Here is a middleware that will add the property asyncDispatch
to all of your actions. When your reducer has finished and returned the new application state, asyncDispatch
will trigger store.dispatch
with whatever action you give to it.
// This middleware will just add the property "async dispatch" to all actions
const asyncDispatchMiddleware = store => next => action => {
let syncActivityFinished = false;
let actionQueue = [];
function flushQueue() {
actionQueue.forEach(a => store.dispatch(a)); // flush queue
actionQueue = [];
}
function asyncDispatch(asyncAction) {
actionQueue = actionQueue.concat([asyncAction]);
if (syncActivityFinished) {
flushQueue();
}
}
const actionWithAsyncDispatch =
Object.assign({}, action, { asyncDispatch });
const res = next(actionWithAsyncDispatch);
syncActivityFinished = true;
flushQueue();
return res;
};
Now your reducer can do this:
function reducer(state, action) {
switch (action.type) {
case "fetch-start":
fetch('wwww.example.com')
.then(r => r.json())
.then(r => action.asyncDispatch({ type: "fetch-response", value: r }))
return state;
case "fetch-response":
return Object.assign({}, state, { whatever: action.value });;
}
}
Upvotes: 239
Reputation: 5170
Dispatching an action within a reducer is an anti-pattern. Your reducer should be without side effects, simply digesting the action payload and returning a new state object. Adding listeners and dispatching actions within the reducer can lead to chained actions and other side effects.
Sounds like your initialized AudioElement
class and the event listener belong within a component rather than in state. Within the event listener you can dispatch an action, which will update progress
in state.
You can either initialize the AudioElement
class object in a new React component or just convert that class to a React component.
class MyAudioPlayer extends React.Component {
constructor(props) {
super(props);
this.player = new AudioElement('test.mp3');
this.player.audio.ontimeupdate = this.updateProgress;
}
updateProgress () {
// Dispatch action to reducer with updated progress.
// You might want to actually send the current time and do the
// calculation from within the reducer.
this.props.updateProgressAction();
}
render () {
// Render the audio player controls, progress bar, whatever else
return <p>Progress: {this.props.progress}</p>;
}
}
class MyContainer extends React.Component {
render() {
return <MyAudioPlayer updateProgress={this.props.updateProgress} />
}
}
function mapStateToProps (state) { return {}; }
return connect(mapStateToProps, {
updateProgressAction
})(MyContainer);
Note that the updateProgressAction
is automatically wrapped with dispatch
so you don't need to call dispatch directly.
Upvotes: 191
Reputation: 3226
You might try using a library like redux-saga. It allows for a very clean way to sequence async functions, fire off actions, use delays and more. It is very powerful!
Upvotes: 18