Pretty new to redux saga - I'm trying to figure out how to call a saga outside the redux middleware environment when I have access to the store.
After reading the redux saga docs looks like I have two options either call store.runSaga or use the runSaga utility provided by redux-saga.
This is what I'm trying to do:
Steps:
Created a saga that pauses until the success action is dispatched.
Something like this:
export function* loadDashboardSequenced() {
try {
// Take pauses until action received.
//Wait for the user to be loaded
const user_success = yield take('FETCH_USER_SUCCESS');
// ....do other stuff....
} catch(error) {
yield put({type: 'FETCH_FAILED', error: error.message});
}
Now I'm trying to call the saga either via store.runSaga https://redux-saga.js.org/docs/api/ or runSaga https://redux-saga.js.org/docs/api/index.html#runsagaoptions-saga-args
Is there any benefits of using runSaga vs store.runSaga? I'm not sure which one I should use at this point. Any thoughts/suggestions? Thanks!
EDIT: followup question about using runSaga https://redux-saga.js.org/docs/advanced/UsingRunSaga.html
What does this line mean
subscribe: ..., // this will be used to resolve take Effects
The store.runSaga method is used to start the root saga for the store. It also expects redux-saga to be a store middleware :
export default function configureStore(initialState) {
const sagaMiddleware = createSagaMiddleware()
return {
...createStore(reducer, initialState, applyMiddleware(/* other middleware, */sagaMiddleware)),
runSaga: sagaMiddleware.run
}
}
const store = configureStore()
store.runSaga(rootSaga)
On the other hand, runSaga method is there for connecting redux-saga to non-store objects and interfaces, which is something you rarely do.
In summary, if you need sagas put and take methods to work with redux actions, then you you need to use redux-saga as store middleware.
If you have a saga that retrieves data from a REST API and then hands off the data to a reducer ... then testing your saga without a store makes a lot of sense.
Furthermore, the thing I didn't like about the Beginner Tutorial tests, is that I wanted to test the full stack, client through to server. The tests in "Beginner Tutorial" cannot test that because they do not actually get the results of the saga's execution, just the call/put messages.
I would look at The best way to test Redux Sagas for full discussion, but here is a quick sample that summarizes Phil Herbert's post:
import { runSaga } from 'redux-saga'; // using runSaga to execute the saga
import { testMeSaga } from './sagas'; // the saga under test
test('test testMeSaga by running it', async () => {
// 'recordSaga' is a utility function for running sagas, see it below.
const result = await recordSaga(
testMeSaga,
{ payload: 'your input to the saga' }
);
expect(result.length).toBe(1);
// add more checks here ...
});
// this is the utility function that wraps the running of the saga
async function recordSaga(saga, initialAction) {
const dispatched = [];
await runSaga(
{
dispatch: (action) => dispatched.push(action)
},
saga,
initialAction
).done;
return dispatched;
}
Related
I came into a React project that uses vanilla Redux with middleware. The way it is setup is as follows:
composeEnhancers(applyMiddleware(...middleware.map(f => f(services))))
Now middlware is an array of, well, middleware containing functions. services is an object containing external services that are injected into the middlware functions (api and so on).
The interesting part is the middleware, here is a sample of it:
...
const throwErrorFlow = ({ api }) => ({ dispatch, getState }) => next => async (action) => {
next(action)
if (action.type === actions.THROW_ERROR) {
try {
dispatch(actions.setLoadingSlot({ state: false, context: action.payload.context }))
const context = getState().ui.context
const payload = { location: action.payload.location, error: action.payload.error?.stack, context }
console.log(payload);
await api.context.throwError(payload)
dispatch(actions.setErrorModalVisibility({ payload, visibility: true }))
} catch (error) {
console.log(error);
}
}
}
const middleware = [
middlwareFunction1,
middlwareFunction2,
...
throwErrorFlow
]
export default middleware
Then I created my own test action that returns a test string. I added a similar middlware function as the rest. When dispatching this test action from the UI and logging its result, all I get is: PromiseĀ {<fulfilled>: undefined}
So I tried zooming in a bit. My action is just the following:
export const customAction = payload => ({
type: CUSTOM_ACTION,
payload: payload,
})
And my bit in the middleware is the following:
const customAsyncActionFlow = () => storeAPI => () => action => {
if (action.type === actions.CUSTOM_ACTION) {
console.log(action);
return 'TEST!'
}
}
const middleware = [
middlwareFunction1,
middlwareFunction2,
...
throwErrorFlow,
customActionFlow
]
export default middleware
And I call it from the UI as:
console.log(dispatch(customAction('Hello World!')));
My action is logged correctly to the console, but then I get PromiseĀ {<fulfilled>: undefined} instead of 'TEST!'. So I removed all other middleware functions and only kept my customActionFlow, and everything worked as I expected. Where is this Promise with no result coming from? Yes all other middleware functions do not return anything, they just modify the state. Does this have to do with this fact? And how do I 'fix' this?
EDIT: okay so I seem to understand what is going on. For each action that requires interaction with the api, a middleware is written for this action which gets applied. In the end there are 20 middleware functions all culminating with the async action for each one. The action that I defined with the test middleware that returns a value gets "lost" in the mix I guess? I am still not sure as to why my return has no effect whatsoever.
Is there a way to make my dispatch action call the my test middleware exclusively while keeping all other middlewares applied?
Oh dear. While this isn't a direct answer to your question...
I've seen that style of "write all Redux logic as custom middleware" tried a few times... and it is a bad idea!
It makes things highly over-complicated, and adding all these extra middleware for individual chunks of functionality adds a lot of overhead because they all have to run checks for every dispatched action.
As a Redux maintainer I would strongly recommend finding better approaches for organizing and defining the app logic. See the Redux Style Guide for our general suggestions:
https://redux.js.org/style-guide/
Now, as for the actual question:
When you call store.dispatch(someAction), the default behavior is that it returns the action object.
When you write a middleware, that can override the return value of store.dispatch(). A common example of this is the redux-thunk middleware, which just does return thunkFunction(dispatch, getState). This is commonly used to let thunks return promises so that the UI knows when some async logic is complete:
https://redux.js.org/tutorials/essentials/part-5-async-logic#checking-thunk-results-in-components
https://redux.js.org/tutorials/fundamentals/part-7-standard-patterns#thunks-and-promises
In this case, the middleware is itself defined as an async function, and every async function in JS automatically returns a promise. So, just having one async middleware in the chain is going to end up returning a promise from store.dispatch(anything). (This would be another reason to not write a bunch of logic directly in a custom middleware like that.)
I created a react app using create-react-app and configured a redux store with reducers. I also added firebase and my project works fine. The components can trigger an action that fetches a collection from firestore, and it in return, updates the redux store.
What is the best way to integrate firebase and redux store?
The way I am currently doing it, is to have a separate action that triggers the fetch/delete/onSnapshot from firebase, and handing a reference to dispatch so that firebase function can take its time executing the command, then it can call dispatch with an action that updates the store.
But I wanted all of my actions in a single file, for a better (separation of concerns). Therefor, firebase can call dispatch but the action creator lives in my actions.js file. This way, I can later decide to change the action names in a single file, if I decided to do that.
The problem with this approach, is I will require a separate action to trigger the async function with firebase, and another action creator for when the promise is fulfilled.
What is a better approach to what I am doing?
store.js
const rootReducer = combineReducers({
cards: cardsReducer,
});
const store = createStore( rootReducer , {}, applyMiddleware(thunk));
export default store;
myFirebase.js
// I need this to be called from an action in actions.js
// therefor, I am exporting it, and also, I am handing it dispatch
// so it will call store.dispatch once data is ready
export const fetchCardsFromFirebase = async (dispatch) => {
const cardsCollection = collection(db, "cards");
const cardsSnapshot = await getDocs(roomsCollection);
const cards = roomsSnapshot.docs.map(doc => ({ ...doc.data(), id: doc.id }));
// here I can either explicitly dispatch an action
/*
dispatch({
type: CARDS_FETCHED //this constant string will have to be imported
payload: cards
});
*/
// or I can let an action in actions.js do the above:
dispatch(cardsFetched(rooms)); //this function is imported from actions.js
}
actions.js
import { FETCH_CARDS , CARDS_FETCHED } from "./types";
import { fetchCardsFromFirebase } from "../myFirebase";
export const fetchCards = () => async (dispatch) => {
fetchCardsFromFirebase(dispatch); // give firebase access to dispatch
dispatch({
type: FETCH_CARDS,
payload: {message: "fetching cards... please wait"}
});
};
const cardsFetched = (cards) => ({
action: CARDS_FETCHED,
payload: cards
});
Generally, this is a very old style of Redux - modern Redux does not use switch..case reducers or ACTION_TYPES and switching to modern Redux will proably already save you 50% of your code.
That said, the official Redux Toolkit (RTK) also comes with RTK-Query, which is a data caching abstraction that should also work fine with firebase and will generate reducers, actions and even hooks automatically for you. (Hint: with firebase you will need to use queryFn). That would save you a lot more code as well.
I would recommend you to follow the official Redux Tutorial which first shows modern Redux and in later chapters goes into RTK Query.
I am using redux-saga having spent some time on core concepts of generators, generators with promises, and redux-saga itself. What I want below is to understand what is idiomatic and recommended, and what isn't.
In one file I define my root saga, watcher saga, and one worker saga (fetchSupplierOrders).
import {
fetchSupplierOrders,
} from './supplierOrders';
import { takeLatest} from 'redux-saga/effects';
function* watchSupplierOrdersSagas() {
yield takeLatest('REQUEST_FETCH_SUPPLIER_ORDERS', fetchSupplierOrders);
}
export default function* rootSaga() {
yield all([watchSupplierOrdersSagas()]);
}
Here is the worker saga:
export function* fetchSupplierOrders() {
try {
const supplierOrders = yield call(getSupplierOrders); // API call is in getSupplierOrders
// normally here I would use redux-saga put to hit my redux reducers
yield supplierOrders.map(({ id }) => id)
} catch (error) {
yield put({ type: 'RECEIVE_ERROR_FETCH_SUPPLIER_ORDERS', error: error.message });
}
}
I have a React component that when I click a button, it executes the worker saga. What I am trying to do here is to not go through the redux-saga watcher saga at all. I will simply execute the generator function myself in the component, and iterate through it. Usually, I would go through the watcher saga, and it would call a worker saga that would generate side effects by modifying redux state.
However, what if I want to make a network request, but I don't want to save the result in redux, but in local component state? I want the component to somehow get the results from the worker saga directly.
Here is the click handler for the button in the React component:
const handleFetchSuppliers = event => {
const it = fetchSupplierOrders({ payload: event.target.value });
const res1 = await it.next().value;
console.log('res1', res1);
const res2 = it.next(res1);
console.log('res2', res2);
This code will not work, because in the worker saga I am using redux-saga's call function. If I remove the use of call, and call getSupplierOrders (an async function) directly, then the await works and all the correct values are console.logged.
Is it common to do this (executing a worker saga from a component to get the results of an API request)? But if I do it this way then I lose the benefit of using call (isn't this useful because it's easier to test?)
Before redux-saga I would simply dispatch a thunk using redux-thunk, which is basically using async/await all the way through.
Do people mix the use of redux-thunk and redux-saga? Is this frowned upon?
However, what if I want to make a network request, but I don't want to save the result in redux, but in local component state?
If redux is not involved, then redux-saga is not the right tool to use. Just use the normal react approach: make the api request (often in componentDidMount), then wait for that promise to complete (with .then or await), then call setState.
If you want to have multiple ways to do the fetch (both via a saga, and via a direct call), then you could put the fetch into a helper function (regular function, not generator). The component and the saga could then both make use of the helper function, each wrapping it with whatever extra work they need to do.
For example:
// helper
async function fetchStuff() {
const response = await fetch('some Url');
if (!response.ok) {
throw response.status;
}
const data = await response.json();
return data.map(({ id }) => id);
}
// In a saga...
function* watchFetch() {
yield takeLatest('doFetch', fetchStuffSaga);
}
function* fetchStuffSaga() {
try {
const data = yield call(fetchStuff);
yield put({ type: 'success', payload: data });
} catch (err) {
yield put({ type: 'error', payload: err });
}
}
// In a component that dispatches an action:
componentDidMount() {
this.props.dispatch({ type: 'doFetch' });
}
// In a component that doesn't want to dispatch an action:
async componentDidMount() {
try {
const data = await fetchStuff();
this.setState({ data });
} catch (err) {
this.setState({ error: err });
}
}
This code will not work, because in the worker saga I am using redux-saga's call function. If I remove the use of call, and call getSupplierOrders (an async function) directly, then the await works and all the correct values are console.logged.
Sagas are not meant for manual iteration. If you try to manually iterate through a saga, you either need to have specialized knowledge about exactly what the saga will yield in what order, or you basically need to re-implement redux-saga yourself. The former is brittle and tightly coupled, the latter is a waste of effort.
Is it common to do this (executing a worker saga from a component to get the results of an API request)?
No.
Do people mix the use of redux-thunk and redux-saga? Is this frowned upon?
They're both trying to handle the same kinds of things (asynchronous actions). Your codebase will be simpler if you use just one approach, then trying to mix and match both.
I am trying to load data when my component loads using componentDidMount. However calling the Redux action, making the call with axios seems to freeze the UI. When I have a form with 12 inputs and one makes an API call I would assume I can type in the other inputs and not have them freeze up on me.
I've tried reading some other posts on the subject but they are all a little different and everything I have tried doesn't seem to resolve the issue.
I am working on linux using React 16.8 (when using RN I use 55.4)
I have tried making my componentDidMount async as well as the redux-thunk action. It didn't seem to help anything, so I must be doing something wrong.
I tried doing the following with no success. Just using short form for what I tried. Actual code listed below.
async componentDidMount() {
await getTasks().then();
}
And I tried this
export const getTasks = () => (async (dispatch, getState) => {
return await axios.get(`${URL}`, AJAX_CONFIG).then();
}
Current Code:
Component.js
componentDidMount() {
const { userIntegrationSettings, getTasks } = this.props;
// Sync our list of external API tasks
if (!isEmpty(userIntegrationSettings)) {
getTasks(userIntegrationSettings.token)
// After we fetch our data from the API create a mapping we can use
.then((tasks) => {
Object.entries(tasks).forEach(([key, value]) => {
Object.assign(taskIdMapping, { [value.taskIdHuman]: key });
});
});
}
}
Action.js
export const getTasks = () => ((dispatch, getState) => {
const state = getState();
const { token } = state.integrations;
const URL = `${BASE_URL}/issues?fields=id,idReadable,summary,description`;
const AJAX_CONFIG = getAjaxHeaders(token);
dispatch(setIsFetchingTasks(true));
return axios.get(`${URL}`, AJAX_CONFIG)
.then((response) => {
if (!isEmpty(response.data)) {
response.data.forEach((task) => {
dispatch(addTask(task));
});
return response.data;
} else {
dispatch(setIsFetchingTasks(false));
}
})
.catch((error) => {
dispatch(setIsFetchingTasks(false));
errorConsoleDump(error);
errorHandler(error);
});
});
reducer.js
export default (state = defaultState, action) => {
switch (action.type) {
case ADD_TASK:
case UPDATE_TASK:
return update(state, {
byTaskId: { $merge: action.task },
isFetching: { $set: false }
});
default:
return state;
}
};
So in my answer what are you going to learn?
General data loading with Redux
Setting up a component lifecycle method such as componentDidMount()
Calling an action creator from componentDidMount()
Action creators run code to make an API request
API responding with data
Action creator returns an action with the fetched data on the payload property
Okay, so we know there are two ways to initialize state in a Reactjs application, we can either invoke a constructor(props) function or we can invoke component lifecycle methods. In this case, we are doing component lifecycle methods in what we can assume is a class-based function.
So instead of this:
async componentDidMount() {
await getTasks().then();
}
try this:
componentDidMount() {
this.props.fetchTasks();
}
So the action creators (fetchTasks()) state value becomes the components this.props.fetchTasks(). So we do call action creators from componentDidMount(), but not typically the way you were doing it.
The asynchronous operation is taking place inside of your action creator, not your componentDidMount() lifecycle method. The purpose of your componentDidMount() lifecycle method is to kick that action creator into action upon booting up the application.
So typically, components are generally responsible for fetching data via calling the action creator, but it's the action creator that makes the API request, so there is where you are having an asynchronous JavaScript operation taking place and it's there where you are going to be implementing ES7 async/await syntax.
So in other words it's not the component lifecycle method initiating the data fetching process, that is up to the action creator. The component lifecycle method is just calling the action creator that is initiating the data fetching process a.k.a. the asynchronous request.
To be clear, you are able to call this.props.fetchTasks() from your componentDidMount() lifecycle method after you have imported the action creator to your component like and you have imported the connect function like so:
import React from "react";
import { connect } from "react-redux";
import { fetchTasks } from "../actions";
You never provided the name of the component you are doing all this in, but at the bottom of that file you would need to do export default connect(null, { fetchTasks })(ComponentName)
I left the first argument as null because you have to pass mapStateToProps, but since I don't know if you have any, you can just pass null for now.
Instead of this:
export const getTasks = () => (async (dispatch, getState) => {
return await axios.get(`${URL}`, AJAX_CONFIG).then();
}
try this:
export const fetchTasks = () => async dispatch => {
const response = await axios.get(`${URL}`, AJAX_CONFIG);
dispatch({ type: "FETCH_TASKS", payload: response.data });
};
There is no need to define getState in your action creator if you are not going to be making use of it. You were also missing the dispatch() method which you need when developing asynchronous action creators. The dispatch() method is going to dispatch that action and send it off to all the different reducers inside your app.
This is also where middleware such as Redux-Thunk comes into play since action creators are unable to process asynchronous requests out of the box.
You did not show how you wired up your redux-thunk, but it typically goes in your your root index.js file and it looks like this:
import React from "react";
import ReactDOM from "react-dom";
import "./index.scss";
import { Provider } from "react-redux";
import { createStore, applyMiddleware } from "redux";
import thunk from "redux-thunk";
import App from "./components/App";
import reducers from "./reducers";
const store = createStore(reducers, applyMiddleware(thunk));
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.querySelector("#root")
Remember that connect function I said you needed to implement? That came into being as a result of implementing or you should have implemented the Provider tag. With the Provider tag, your components can all have access to the Redux store, but in order to hook up the data to your components you will need to import the connect function.
The connect function is what reaches back up to the Provider and tells it that it wants to get access to that data inside whatever component you have that lifecycle method in.
Redux-Thunk is most definitely what you needed to implement if you have corrected everything as I have suggested above.
Why is Redux-Thunk necessary?
It does not have anything intrinsically built into it, it's just an all-purpose middleware. One thing that it does is allow us to handle action creators which is what you need it to be doing for you.
Typically an action creator returns an action object, but with redux-thunk, the action creator can return an action object or a function.
If you return an action object it must still have a type property as you saw in my code example above and it can optionally have a payload property as well.
Redux-Thunk allows you to return either an action or function within your action creator.
But why is this important? Who cares if it returns an action object or a function? What does it matter?
That's getting back to the topic of Asynchronous JavaScript and how middlewares in Redux solves the fact that Redux is unable to process asynchronous JavaScript out of the box.
So a synchronous action creator instantly returns an action with data ready to go. However, when we are working with asynchronous action creators such as in this case, it takes some amount of time for it to get its data ready to go.
So any action creator that makes an network request qualifies as an asynchronous action creator.
Network requests with JavaScript are asynchronous in nature.
So Redux-Thunk, being a middleware which is a JavaScript function that is going to be called with every single action that you dispatch. The middleware can stop the action from proceeding to your reducers, modify the action and so on.
You setup dispatch(setIsFetchingTasks(true)) but when axios returns you never set it to false. Did you miss to add dispatch(setIsFetchingTasks(false)) before return response.data;?
This could be the reason if your UI waits for the fetchingTasks to finish
I am trying to chain multiple actions together in the following fashion:
A. post user data to database
B. use posted data to query Elasticsearch for results
(I do A and B in parallel)
B1. with results from ES, query original database for results from two tables
B2. navigate to new page and update UI
I am using thunks right now to reason about my code, but I also found this async pattern to be extremely verbose:
export function fetchRecipes(request) {
return function(dispatch) {
dispatch(requestRecipes(request))
return fetch(url)
.then(response => response.json())
.then(json => dispatch(receiveRecipes(request, json))
)
}
}
this, along with "requestRecipes" and "receiveRecipes" as other action creators seems like quite a bit just to make one async call. (a request, a receive, and a fetch function)
summary: when you're chaining 2-3 async actions whose outputs depend on each other (I need to promisify when possible), is there a more efficient means of doing so without writing 3 functions for each async call?
I figure there had to be a way. I'm pattern matching off of the Redux docs and soon became very overwhelmed with the functions I was creating
thanks a lot for the feedback!
You can use redux-saga instead of redux-thunk to achieve this more easily. redux-saga lets you describe your work using generators and is easier to reason about.
The first step is to describe how you pass your data to redux without worrying about services or async stuff.
Actions
// actions.js
function createRequestTypes(base) {
return {
REQUEST: base + "_REQUEST",
SUCCESS: base + "_SUCCESS",
FAILURE: base + "_FAILURE",
}
}
// Create lifecycle types on `RECIPES`
export const RECIPES = createRequestTypes("RECIPES")
// Create related actions
export const recipes = {
// Notify the intent to fetch recipes
request: request => ({type: RECIPES.REQUEST, request})
// Send the response
success: response => ({type: RECIPES.SUCCESS, response})
// Send the error
error: error => ({type: RECIPES.FAILURE, error})
}
Reducer
// reducer.js
import * as actions from "./actions"
// This reducer handles all recipes
export default (state = [], action) => {
switch (action.type) {
case actions.RECIPES.SUCCESS:
// Replace current state
return [...action.response]
case actions.RECIPES.FAILURE:
// Clear state on error
return []
default:
return state
}
}
Services
We also need the recipes API. When using redux-saga the simplest way to declare a service is to creating a (pure) function which reads the request as argument and returns a Promise.
// api.js
const url = "https://YOUR_ENPOINT";
export function fetchRecipes(request) {
return fetch(url).then(response => response.json())
}
Now we need to wire actions and services. This is where redux-saga come in play.
// saga.js
import {call, fork, put, take} from "redux-saga/effects"
import * as actions from "./actions"
import * as api from "./api"
function* watchFetchRecipes() {
while (true) {
// Wait for `RECIPES.REQUEST` actions and extract the `request` payload
const {request} = yield take(actions.RECIPES.REQUEST)
try {
// Fetch the recipes
const recipes = yield call(api.fetchRecipes(request))
// Send a new action to notify the UI
yield put(actions.fetchRecipes.success(recipes))
} catch (e) {
// Notify the UI that something went wrong
yield put(actions.fetchRecipes.error(e))
}
}
}
function* rootSaga() {
yield [
fork(watchFetchRecipes)
]
}
And that's it! Whenever a component will send a RECIPES.REQUEST action, the saga will hook up and handle the async workflow.
dispatch(recipes.request(req))
What's awesome with redux-saga is that you can easily chain async effects and dispatch actions during the workflow.
Based on your description, the only time you actually update your UI is right at the end of all these asynchronous operations (B1).
If you don't use the results from the preceding async calls to change your application state / update your UI, what is the benefit of having these fine-grained actions?
Of course there are things like "loading / request started" and "finished loading / request stopped", but it seems to me, that in your case, you could just do the chained async calls outside of redux (in some kind of API-layer) and only use one action.
This action dispatches a "REQUEST_STARTED", then calls the API-layer, which does the DB-calls and elasticsearch request etc., and then dispatches either "REQUEST_SUCCESS" or "REQUEST_FAILURE", based on the result of the promise, which will give you the data you need to update your UI.
This way, the state in redux only concerns itself with ONE side-effect, instead of the implementation details of your chained calls. Also, your action gets a lot simpler, because it just handles the results of one async call.