I have tiredlessly tried everything i can find on stack for this issue and am getting no where. We are using react/typescript. redux, and saga. I have a list of categories to bring back for nav list and using useEffect to dispatch the action to redux store.
our tsx.file:
const dispatch = useDispatch();
const categories = useSelector((state) => state?.categories?.payload);
const loadCategories = () => {
dispatch(getCategories(categories));
};
useEffect(() => {
loadCategories();
}, []);
{categories?.map((x, index) => (
<Link href={"/store/" + `${x.name}` + "/s"}>
<a
type="button"
id={`${x.name}`}
title={`${x.name}`}
className={"xl:px-3 px-2 py-[1.15rem] font-normal"}>
{x.name}
</a>
</Link>
))}
Network traffic just shows hundreds of requests going out to the category endpoint -- stumped!
still stuck so adding our redux/saga files
actions:
import {GET_CATEGORIES} from './actionTypes'
export const getCategories = (categories: any) => {
return {
type: GET_CATEGORIES,
payload: categories,
}
}
reducer:
import {GET_CATEGORIES} from './actionTypes'
const reducer = (state = [], action) => {
switch (action.type) {
case GET_CATEGORIES:
state = {
...state,
payload: action.payload,
}
break
default:
state = {...state}
break
}
return state
}
export default reducer
saga:
let categoriesApiService = container.resolve(CategoriesApiService)
const categoryApi = async () => {
return firstValueFrom(
categoriesApiService.GetCategoryTree({
path: {version: '1'},
query: {},
})
)
}
function* getCategoriesTree() {
try {
let categoryTreeDTO: CategoryTreeDTO = yield call(categoryApi)
yield put(getCategories(categoryTreeDTO))
} catch (error: any) {
yield put(apiError(error?.response?.data?.message))
}
}
export function* watchGetCategories() {
yield takeEvery(GET_CATEGORIES, getCategoriesTree)
}
function* categorySaga() {
yield all([fork(watchGetCategories)])
}
export default categorySaga
The problem is that you are using the same action to start the saga & to store the fetched data:
component: dispatch(getCategories(categories))
saga: yield put(getCategories(categoryTreeDTO))
so every time data are fetched the saga is triggered again.
What you want to do is to have different action for storing the fetched data, e.g.:
yield put(getCategoriesSuccess(categoryTreeDTO))
(also update your action type in reducer)
Related
I am using redux-saga as a middleware and i want to dispatch multiple action items to the store. As of now i am able to dispatch only one action (i.e., within the fetchData() function which i am calling in Home.js component).
I've tried adding multiple actions but its not working, Only the first action type is getting dispatched
action.js
import { FETCH_ABOUT, FETCH_CTA, FETCH_PRODUCTS} from './actionType'
//import axios from 'axios';
export const fetchData = () => (
{ type:FETCH_PRODUCTS}
)
export const fetchProducts = (products) => ({
type: FETCH_PRODUCTS,
payload: products,
})
export const fetchCta = (cta) => ({
type: FETCH_CTA,
payload: cta,
})
export const fetchAbout = (about) => ({
type: FETCH_ABOUT,
payload: about,
})
reducer.js
import { FETCH_ABOUT, FETCH_CTA, FETCH_PRODUCTS } from "./actionType"
const initialState = {
products: [],
cta:'',
about:'',
}
const productsReducer = (state=initialState,action) => {
switch(action.type){
case FETCH_PRODUCTS:
return{
...state,
products: action.payload
}
case FETCH_CTA:
return{
...state,
cta: action.payload
}
case FETCH_ABOUT:
return{
...state,
about: action.payload
}
default:
return state;
}
}
export default productsReducer;
ProductsSaga.js
import {call,fork,put,takeLatest} from 'redux-saga/effects'
import { fetchCta, fetchProducts } from '../redux/action';
import { FETCH_CTA, FETCH_PRODUCTS } from '../redux/actionType';
import { fetchAPIcall } from '../redux/api'
function* fetchData() {
try{
const { data } = yield call(fetchAPIcall);
console.log(data.data.productCopy);
yield put(fetchProducts(data.data.productCopy));
}catch(e){
console.log(e);
}
}
//watcher saga
export function* watcherSaga() {
yield takeLatest(FETCH_PRODUCTS,fetchData)
}
export const productsSaga = [fork(watcherSaga)]
ctaSaga.js
import { call,put,takeLatest,fork } from "redux-saga/effects";
import { fetchCta } from "../redux/action";
import { FETCH_CTA } from "../redux/actionType";
import { fetchAPIcall } from "../redux/api";
function* onFetchCta() {
try{
const { data } = yield call(fetchAPIcall);
console.log(data.data.cta);
yield put(fetchCta(data.data.cta));
}catch(e){
console.log(e);
}
}
//watcher saga
export function* watcherSaga() {
yield takeLatest(FETCH_CTA,onFetchCta)
}
export const ctaSaga = [fork(watcherSaga)]
Home.js
import React, { useEffect, useState } from 'react'
import './Home.css'
import {useHistory} from 'react-router-dom';
import { useSelector,useDispatch } from 'react-redux';
import {fetchData} from './redux/action'
const Home = () => {
const history = useHistory();
const {products,cta} = useSelector((state)=>(state.productsReducer));
console.log(products,cta);
const dispatch = useDispatch();
useEffect(()=>{
dispatch(fetchData());
},[])
const productDetail = (item,i) => {
history.push({
pathname:`product-detail/${i}`,
state:item
})
}
return (
<div className='container'>
<div className='product'>
{products.map((item,i) =>{
return(
<div key={item.id}>
<img src={item.Image.path} alt = {item.Image.alt}/>
<p>{item.title}</p>
<button onClick={()=>productDetail(item,i)}type='button'>{cta}</button>
</div>
)
})}
</div>
</div>
)
}
export default Home
Action creators such as fetchData will always create only a single action object. Also the dispatch function (or put effect) can always dispatch only a single action, however nothing is preventing you from dispatching multiple actions one after the other:
function* mySaga() {
yield put(firstAction())
yield put(secondAction())
yield put(thirdAction())
}
// or
function MyComponent() {
const dispatch = useDispatch()
return <div onClick={() => {
dispatch(firstAction())
dispatch(secondAction())
dispatch(thirdAction())
}}>Hello</div>
}
If you are worried about rerenders, react-redux has the batch function that allows you to wrap your dispatches making sure that react batches all the updates and rerenders only a single time. Note that this is not necessary starting from React 18 as it batches things automatically.
It is more difficult to use the batched updates with redux saga due to its internal scheduler that doesn't guarantee that everything will happen in single tick, but again starting from React 18 you don't need to worry about this. In case you really need it, there are libraries out there that allows you to do it though, check out Mark's post about it which includes links to some of these libraries: https://blog.isquaredsoftware.com/2020/01/blogged-answers-redux-batching-techniques/
One more thing, if you find yourself dispatching the same list of actions again and again, maybe it make sense to merge these together to a single action and avoid the issue entirely.
using nextjs for server-side-rendering trying to get the state from redux store in getServerSideProps(). but getting emtpy value.
getting data from redux in client side inside the component with const productList = useSelector(state => state.productList) const { loading, error, products } = productList works fine. but when using getServersideProps() im getting emtpy results.
index.js:
import store from '../redux/store'
export default function Home({products, error, loading}) {
const dispatch = useDispatch()
useEffect(() => {
dispatch(listProducts())
}, [dispatch])
return (
<>
<Header />
<Products loading={loading} error={error} products={products} />
<Footer />
</>
)
}
export async function getServerSideProps() {
const state = store.getState()
const { loading, error, products } = state.productList
return {props: {products: products, loading: loading, error: error}}
}
*note: even when i did console.log(store.getState()) inside the component its still returning empy array
reducer:
export const productListReducer = (state = { products: [] }, action) => {
switch (action.type) {
case 'PRODUCT_LIST_REQUEST':
return { loading: true, products: [] }
case 'PRODUCT_LIST_SUCCESS':
return { loading: false, products: action.payload }
case 'PRODUCT_LIST_FAIL':
return { loading: false, error: action.payload }
default:
return state
}
}
action:
import axios from 'axios'
export const listProducts = () => async (dispatch) => {
try {
dispatch({ type: 'PRODUCT_LIST_REQUEST' })
const { data } = await axios.get('/api/products')
dispatch({
type: 'PRODUCT_LIST_SUCCESS',
payload: data
})
} catch (error) {
dispatch({
type: 'PRODUCT_LIST_FAIL',
payload: error.response && error.response.data.message
? error.response.data.message : error.message
})
}
}
store.js:
const reducer = combineReducers({
productList: productListReducer,
categoryList: categoryListReducer,
})
const initialState = {}
const middleware = [thunk]
const store = createStore(
reducer, initialState, composeWithDevTools(applyMiddleware(...middleware))
)
export default store
try invoking getState() directly and don't forget to pass it as argument and also make sure you have passed the store to your app component
export async function getServerSideProps(getState) {
const state = getState()
const { loading, error, products } = state.productList
return {props: {products: products, loading: loading, error: error}}
}
The issue is useDispatch is a React-Redux function, but the store has not been connected to the React components.
Instead of useDispatch try store.dispatch instead:
import store from '../redux/store'
export default function Home({products, error, loading}) {
useEffect(() => {
store.dispatch(listProducts())
})
return (
<>
...
</>
)
}
Note, the array passed to useEffect controls when that effect is run, so it would not make sense to pass in the dispatch function. See this post for more details.
You could also connect the Redux store to the React components using React-Redux and keep using useDispatch.
I'm having an issue with useReducer + Typescript + async. I just can't do it! When I call anything from async function it return a Promise which break my code. When I tried to get it other way, the component is doesn't re-render! That is Driving me crazy.
I wrote this issue on my personal project which represents the problem I have! https://github.com/igormcsouza/full-stack-todo/issues/15
What I can do to make it work?
I want to make a call from the backend populate the list with the information I got from backend. So my frontend need to re-render every time any change is done to the backend (when add, update or delete any registry there).
reducers.tsx
import { delete_todo, fetch_todos, insert_todo, update_todo } from
"../utils";
import { State, Actions, Todo } from "../TodoContext";
export const INITIAL_STATE: State = {
todos: [],
};
export const reducer = (state: State, action: Actions): State => {
let newState: State = {};
switch (action.type) {
case "POPULATE":
fetch_todos().then((value) => (newState = value));
return newState;
case "ADD_TODO":
if (state.todos) {
const newTodo: Todo = {
when: (+new Date()).toString(),
task: action.payload,
checked: false,
by: "Igor Souza",
};
insert_todo(newTodo);
}
fetch_todos().then((value) => (newState = value));
return newState;
case "CHECK_TODO":
action.payload.checked = !action.payload.checked;
update_todo(action.payload);
fetch_todos().then((value) => (newState = value));
return newState;
case "EDIT_TODO":
let todo = action.payload.task;
todo.task = action.payload.newTaskName;
update_todo(todo);
fetch_todos().then((value) => (newState = value));
return newState;
case "DELETE_TODO":
delete_todo(action.payload);
fetch_todos().then((value) => (newState = value));
return newState;
default:
return state;
}
};
utils.tsx (with the axios calls)
import axios from "axios";
import { State, Todo } from "./TodoContext";
// const base = "http://backend:2500";
const base = "https://full-stack-todo-bknd.herokuapp.com";
export async function fetch_todos(): Promise<State> {
let todos: State = {};
await axios
.get<State>(base + "/api/todo")
.then((response) => {
const { data } = response;
todos = data;
})
.catch((e) => console.log(e));
console.log(typeof todos.todos);
return todos;
}
export async function insert_todo(todo: Todo) {
await axios.post(base + "/api/todo", todo).catch((e) => console.log(e));
}
export async function update_todo(todo: Todo) {
await axios.put(base + "/api/todo/" + todo.id).catch((e) => console.log(e));
}
export async function delete_todo(todo: Todo) {
await axios
.delete(base + "/api/todo/" + todo.id)
.catch((e) => console.log(e));
}
context.tsx (Context APi)
import React, { createContext, useReducer } from "react";
import { reducer, INITIAL_STATE } from "./reducers";
type ContextProps = {
state: State;
dispatch: (actions: Actions) => void;
};
export interface Todo {
id?: string;
task: string;
when: string;
checked: boolean;
by: string;
}
export interface State {
todos?: Array<Todo>;
}
export interface Actions {
type: string;
payload?: any;
}
export const TodoContext = createContext<Partial<ContextProps>>({});
const TodoContextProvider: React.FC = ({ children }) => {
const [state, dispatch] = useReducer(reducer, INITIAL_STATE);
return (
<TodoContext.Provider value={{ state, dispatch }}>
{children}
</TodoContext.Provider>
);
};
export default TodoContextProvider;
Put simply, what you are trying to do is not possible. You cannot have a reducer that is asynchronous. This means that you need to move the async logic outside of the reducer itself.
The reducer is just responsible for applying the data from the action to the state. Since you are re-fetching the whole list after every action (not ideal) you only have one real action which is to replace the whole state. You would do the aysnc fetching and then refresh the state.
export const populate = (dispatch: Dispatch<Actions>) => {
fetch_todos().then((data) =>
dispatch({
type: "POPULATE",
payload: data
})
);
};
export const reducer = (state: State, action: Actions): State => {
switch (action.type) {
case "POPULATE":
return action.payload;
...
<button onClick={() => populate(dispatch)}>Populate</button>
Passing the dispatch function to an action creator is called a "thunk" and it's a popular pattern with Redux. We don't have any middleware, so we just directly call populate(dispatch) instead of something like dispatch(populate()).
Look for ways that you can streamline your code.
We can make use of the fact that all our actions call the same fetch_todos() in order to simplify things (for now -- eventually you want to not refresh the entire list after every change).
insert_todo, update_todo, and delete_todo are all extremely similar. The main difference is the axios method which can be passed as an argument with axios.request.
Though the more I look, the more I see that they should be less similar! You need to pass the todo data on your put request. You want the id property on Todo to be required and for add_todo to take Omit<Todo, 'id'>.
The inverted approach would be to make changes directly to the reducer state first. Then use a useEffect to detect changes and push the to the backend.
I am using Redux for state management and saga as a middleware. For some reason my app is in some infinite loop state of calling API endpoint.
This is my actions:
export const GET_USERS = "GET_USERS";
export const getUsers = () => ({
type: GET_USERS,
});
export const GET_USERS_SUCCESS = `${GET_USERS}_SUCCESS`;
export const getUsersSuccess = (data) => ({
type: GET_USERS_SUCCESS,
payload: data,
});
export const GET_USERS_FAIL = `${GET_USERS}_FAIL`;
export const getUsersFail = (error) => ({
type: GET_USERS_FAIL,
payload: error,
});
This is saga:
export function* getUsers$() {
try {
const users = yield getUsersAPI();
yield put(actions.getUsersSuccess(users.data));
} catch (error) {
yield put(actions.getUsersFail(error));
}
}
export default function* () {
yield all([takeLatest(actions.getUsers, getUsers$)]);
}
This is a reducer:
export default (state = initialState(), action) => {
const { type, payload } = action;
switch (type) {
case actions.GET_USERS:
return {
...state,
users: {
...state.users,
inProgress: true,
},
};
case actions.GET_USERS_SUCCESS:
return {
...state,
users: {
inProgress: false,
data: payload,
},
};
case actions.GET_USERS_FAIL:
return {
...state,
users: {
...state.users,
inProgress: false,
error: payload,
},
};
default:
return state;
}
};
And this is a component connected with redux:
const Home = (props) => {
useEffect(() => {
props.getUsers();
console.log('props', props.data);
}, []);
return(
<h1>Title</h1>
);
}
const mapStateToProps = ({
users: {
users: {
data
}
}
}) => ({data})
export default connect(mapStateToProps, {getUsers})(Home);
Why is this happening?
This is due to the fact that you misused the sagas in your example. As with any other effect creator as the first parameter must pass a pattern, which can be read in more detail in the documentation. The first parameter can also be passed a function, but in a slightly different way. View documentation (block take(pattern)).
In your case, you are passing a function there that will return an object
{
type: 'SOME_TYPE',
payload: 'some_payload',
}
Because of this, your worker will react to ALL events that you dispatch.
As a result, you receive data from the server, dispatch a new action to save data from the store. And besides the reducer, your getUsers saga will be called for this action too. And so on ad infinitum.
Solution
To solve this problem, just use the string constant actions.GET_USERS that you defined in your actions.
And your sagas will look like this:
export function* getUsers$() {
try {
const users = yield getUsersAPI();
yield put(actions.getUsersSuccess(users.data));
} catch (error) {
yield put(actions.getUsersFail(error));
}
}
export default function* () {
yield all([takeLatest(actions.GET_USERS, getUsers$)]);
}
This should fix your problem.
Until now I've been using redux-thunk for async actions. On application startup I use to have to load some data from some server. So what I do is to create async actions and then use async/await in order to know when they finished. While async actions are fetching I render a splashscreen. When they finish then I start the application.
Now I'm switching to redux sagas and I don't know how to do it with them. I cannot use async/await. What I thought is to have a boolean var in every object of the store that needs to fetch data. However I would like to know if there is any pattern to manage it in a clean way. Does anybody know any pattern for this purpose?
// example with thunks
import { someAsyncAction, someAsyncAction2 } from './actions';
const initialDispatches = async (store) => {
await store.dispatch(someAsyncAction());
await store.dispatch(someAsyncAction2());
};
export default initialDispatches;
In my opinion there's no right/wrong pattern in this kind of cases.
Iv'e put up an example for you of how your goal could be achieved using saga.
The basic idea: have a separate saga for each resource (for instance, I used to split into feature sagas), and a saga for the initialization.
Then the main root saga will run them all parallelly, and you will be able to trigger the initialization saga somewhere in your app and let it all happen:
Note: this example is super naive and simple, you should find a better way for organizing everything up, I just tried to keep it simple.
const {Provider, connect} = ReactRedux;
const {createStore, applyMiddleware} = Redux;
const createSagaMiddleware = ReduxSaga.default;
const {takeEvery, takeLatest} = ReduxSaga;
const {put, call, all, fork} = ReduxSaga.effects;
const initialState = {
fruits: [],
vegtables: []
};
const reducer = (state = initialState, action) => {
switch (action.type) {
case 'SET_FRUITS':
return {
...state,
fruits: [
...action.payload.fruits
]
}
case 'SET_VEGTABLES':
return {
...state,
vegtables: [
...action.payload.vegtables
]
}
}
return state;
};
//====== VEGTABLES ====== //
async function fetchVegtables() {
return await new Promise((res) => {
setTimeout(() => res([
'Cuecumber',
'Carrot',
'LEttuce'
]), 3000)
});
}
function* getVegtables() {
const vegtables = yield call(fetchVegtables);
yield put({ type: 'SET_VEGTABLES', payload: { vegtables } })
}
function* vegtablesSaga() {
yield takeEvery('GET_VEGTABLES', getVegtables);
}
//====== VEGTABLES ====== //
//====== FRUITS ====== //
async function fetchFruits() {
return await new Promise((res) => {
setTimeout(() => res([
'Banana',
'Apple',
'Peach'
]), 2000)
});
}
function* getFruits() {
const fruits = yield call(fetchFruits);
console.log(fruits)
yield put({ type: 'SET_FRUITS', payload: { fruits } })
}
function* fruitsSaga() {
yield takeEvery('GET_FRUITS', getFruits);
}
//====== FRUITS ====== //
//====== INIT ====== //
function* initData() {
yield all([
put({ type: 'GET_FRUITS' }),
put({ type: 'GET_VEGTABLES' })
]);
}
function* initSaga() {
yield takeLatest('INIT', initData);
}
//====== INIT ====== //
// Sagas
function* rootSaga() {
yield all([
yield fork(initSaga),
yield fork(fruitsSaga),
yield fork(vegtablesSaga),
]);
}
// Component
class App extends React.Component {
componentDidMount() {
this.props.dispatch({ type: 'INIT' });
}
render () {
return (
<div>
<div>fruits: {this.props.fruits.join()}</div>
<div>vegtables: {this.props.vegtables.join()}</div>
</div>
);
}
}
// Store
const sagaMiddleware = createSagaMiddleware();
const store = createStore(
reducer,
applyMiddleware(sagaMiddleware)
);
sagaMiddleware.run(rootSaga);
const ConnectedApp = connect((state) => ({
fruits: state.fruits,
vegtables: state.vegtables
}))(App);
// Container component
ReactDOM.render(
<Provider store={store}>
<ConnectedApp />
</Provider>,
document.getElementById('root')
);
As you can see, I have two resources: fruits and vegetables.
Each resource has it's own saga, which is responsible for watching for GET actions dispatched somewhere.
Each of them using basic saga effects such as call, put etc to fetch the resources asyncly, and then they dispatch it to the store (and then the reducer handles them).
In addition, Iv'e set up an initSaga which uses the all effect to trigger all of the resource fetching sagas in a parallel way.
You can see the whole example running here:
https://jsfiddle.net/kadoshms/xwepoh5u/17/
I wrote about creating a structure on top of redux-saga to facilitate async operations by providing an initial action and then loading/success/error states based on the result of the operation. It's in 2 parts, first sync and then async.
It basically lets you write your reducers declaratively, like an object. You only have to call the initial action and the saga takes care of the rest and your UI can respond to the results when loading/success/error actions are triggered. Below is what the reducer looks like.
const counterAsync = {
initialState: {
incrementAsync_result: null,
incrementAsync_loading: false,
incrementAsync_success: false,
incrementAsync_error: false,
},
incrementAsync: {
asyncOperation: incrementAPI,
action: ({number}) => {
type: ACTION_INCREMENT_ASYNC,
payload: {
number: number
}
}
loading: {
action: (payload) => {
return {
type: ACTION_INCREMENT_ASYNC,
payload: { ...payload }
}
},
reducer: (state, action) => {
state.incrementAsync_loading = true
state.incrementAsync_success = false
state.incrementAsync_error = false
}
},
success: {
action: (payload) => {
return {
type: ACTION_INCREMENT_ASYNC,
payload: { ...payload }
}
},
reducer: (state, action) => {
state.incrementAsync_result = action.payload
state.incrementAsync_loading = false
state.incrementAsync_success = true
state.incrementAsync_error = false
}
},
fail: {
action: (payload) => {
return {
type: ACTION_INCREMENT_ASYNC,
payload: { ...payload }
}
},
reducer: (state, action) => {
state.incrementAsync_result = action.payload
state.incrementAsync_loading = false
state.incrementAsync_success = false
state.incrementAsync_error = true
}
}
},
}
We use a slightly more heavy weight version of this pattern at work and it's much better than vanilla redux/saga.
Let me know if you have any questions!
https://medium.com/#m.razajamil/declarative-redux-part-1-49a9c1b43805
https://medium.com/#m.razajamil/declarative-redux-part-2-a0ed084e4e31