ref – https://www.youtube.com/watch?v=4s2bTUbKNUs
https://stackoverflow.com/questions/71713111/mui-installation-doesnt-work-with-react-18
demo (search keyword and receives data)
Create the app.
npx create-react-app recipe-app-redux
cd receipe-app-redux
Install packages required to build this app.
npm i react-redux redux redux-saga redux-logger
npm i axios
npm install @mui/material @emotion/react @emotion/styled –legacy-peer-deps
npm install @mui/icons-material –legacy-peer-deps
First, we create a saga middleware.
then we insert the middleware into the store.
Finally, we run imported root saga in our saga middleware.
src/redux/store.js
1 2 3 4 5 6 7 8 9 10 11 |
import rootSaga from "./sagas"; // 1) create the saga middle ware const sagaMiddleWare = createSagaMiddleware(); // 2) insert the middleware into the store const middleware = [sagaMiddleWare]; const store = createStore(rootReducer, applyMiddleware(...middleware)); // 3) run root saga in saga middleware sagaMiddleWare.run(rootSaga); |
First know that whenever we dispatch an action for type FETCH_RECIPE_START, we will execute onLoadRecipeAsync.
takeLatest listens for action types (i.e FETCH_RECIPE_START), and then execute the corresponding function (i.e onLoadRecipeAsync)
(note: for takelatest, if task A was started previously and still running, we cancel the current one)
Hence takelatest is like registering for event listeners. Thus we need to execute this ‘registration’ via fork. Fork is non-blocking because
we want to asynchronously do this registration.
We then use all to make sure all “async registrations” (i.e recipeSaga) are run in parallel and wait for all of them to complete.
fork vs call
First, some concepts.
ref –
- https://stackoverflow.com/questions/47798900/redux-saga-call-vs-fork-and-join
- https://stackoverflow.com/questions/42938520/redux-saga-when-to-use-fork/42961360#42961360
1 2 3 4 5 6 7 8 9 |
function* main() { yield fork(someSaga); console.log('this won't wait for the completion of someSaga'); } function* main() { yield call(someSaga); console.log('this will wait for the completion of someSaga'); } |
src/redux/sagas.js
1 2 3 4 5 6 7 8 9 |
export function* onLoadRecipe() { yield takeLatest(types.FETCH_RECIPE_START, onLoadRecipeAsync); } const recipeSaga = [fork(onLoadRecipe)]; // non-blocking export default function* rootSaga() { yield all([...recipeSaga]); } |
so essentially, what we’re doing here is to make sure we register all takeLatest, which registers event listeners for action type to functions.
Now, let’s look at the worker function onLoadRecipeAsync. What we’re trying to do here is to run a getRecipes function with some query data. getRecipes just makes a request to a server somewhere. THEN, we put the response data into the reducer. These two steps must be synced. Step 1 must finish first, then step 2 can go.
Hence that’s why we use call. getRecipes itself is a Promise, so it runs asynchronously, but we use call to pause saga so that the next saga effect will wait for the response of this saga effect.
src/redux/sagas.js
1 2 3 4 5 6 7 8 9 10 11 12 13 |
export function* onLoadRecipeAsync({ query }) { try { // we use call here because we MUST (block) make sure the next line of code waits for the response: they must be sync. // 1) we query and get data first. We need to WAIT for the return of this data. const response = yield call(getRecipes, query); // is blocking // if it returns a promise, it pauses the saga until the promise is resolved. // 2) Only then, put data into reducer (non-blocking) yield put({type: types.FETCH_RECIPE_SUCCESS, payload: response.data}); } catch (error) { } } |
After receiving the response, we can then use put to put the data into the reducer. put is non-blocking. (We have received the data already…storing data can be done asynchronously here)
Fetching the Data
The word non-blocking is usually used with I/O, while asynchronous is used in a more generic sense with a broader range of operations.
But few major differences are:1) Asynchronous does not respond immediately, While Nonblocking responds immediately if the data is available and if not that simply returns an error.2) Asynchronous improves the efficiency by doing the task fast as the response might come later, meanwhile, can do complete other tasks.
Fetching the data is simply a Promise. It asynchronously does an I/O operation for fetch in the web environment using axios. Thus, this is non-blocking.
src/redux/api.js
1 2 3 4 5 |
// returns a promise export const getRecipes = async (query) => { const url = `https://api.edamam.com/search?q=${query}&app_id=${APP_ID}&app_key=${APP_KEY}&from=0&to=3&calories=591-722&health=alcohol-free`; return await axios.get(url); }; |
Notice that we use call to execute getRecipes which returns a promise. call is blocking, while getRecipes is async. So is this all blocked or simply all async? The answer is that due to getRecipes being an I/O operation for the web environment, it is non-blocking. So UI still functions. The blocking part from call pauses the saga middleware so that future middleware must wait for its data response.
Implementing the UI
todo – use Antd for UI since now we have all the data.