I fetched some data from my api by react-redux. My problem is that, since it is async I have to wait for the state to update its inital value in order to use them in the app. For example I have to use
products && products.length && products[n].img
syntax not to get undefined error when I try to access the fetched data. But when I use them at the first render just as
products[n].img
the app gives undefined as it should because redux fetches the data asynchronously. How can I bypass these steps so that I can use my desired state immediately?
React code
import React, { useEffect } from "react";
import {useDispatch, useSelector} from 'react-redux'
import { listPoduct } from "../actions/productActions";
const Examples = () => {
const dispatch = useDispatch()
const productList = useSelector(state => state.productList)
const {loading, error, products} = productList
useEffect(()=>{
dispatch(listPoduct())
},[dispatch])
console.log(products && products.length && products[0].img)
return(
<div>
...
</div>
)
}
export default Examples
Action
export function listPoduct() {
return (dispatch) => {
const baseUrl = "/api/images"
fetch(`${baseUrl}`)
.then(res => res.json())
.then(res => {
dispatch({
type: PRODUCT_LIST_SUCCESS,
payload: res
})
})
}
}
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
}
}
Store
import {createStore, combineReducers, applyMiddleware} from 'redux'
import thunk from 'redux-thunk'
import {composeWithDevTools} from 'redux-devtools-extension'
import {productListReducer, productDetailsReducer} from './reducers/productReducer'
const reducer = combineReducers({
productList: productListReducer,
productDetails: productDetailsReducer
})
const initialState = {}
const middleware = [thunk]
const store = createStore(
reducer,
initialState,
composeWithDevTools(applyMiddleware(...middleware))
)
export default store
The short answer is that you cannot. Sadly.
Your request is asynchronous, so there's just no data available immediately.
In your particular case, my advice would be to render some kind of spinner-loader conditionally (if loading is set to true) and only the loader.
In this case, if you have loading set to true, you will not reach the place where you can actually read the data (you will render-and-return before). And once loading switches back to false, you can now display the data safely as the request is finished and the data is in the right place.
The same applies to the failure state (as there's also no data available if the request failed).
Here's your modified code (as an example):
const Examples = () => {
const dispatch = useDispatch()
const productList = useSelector(state => state.productList)
const {loading, error, products} = productList
useEffect(()=>{
dispatch(listPoduct())
},[dispatch]);
if (loading) {
return (<div>Loading...</div>);
}
if (error) {
return (<div>Error: {error}</div>);
}
console.log(products && products.length && products[0].img)
return(
<div>
...
</div>
)
}
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.
I want to use the error value as an alert in my application. my server response also gave me the correct response. but when used in reacts error value shows undefined.
<b>Response Getting from API request</b>
{"success":false,"error":"Product Not Found"}
<b>code in react file</b>
const alert = useAlert();
const dispatch = useDispatch();
const { loading, error, products } = useSelector((state) => state.products);
useEffect(() => {
if (error) {
alert.error(error);
}
dispatch(getProduct());
}, [dispatch, error, alert]);
<b>Reducer</b>
export const productReducer = (state = { products: [] }, action) => {
switch (action.type) {
case ALL_PRODUCT_REQUEST:
return {
loading: true,
products: [],
};
case ALL_PRODUCT_SUCCESS:
return {
loading: false,
products: action.payload.products,
productsCount: action.payload.productsCount,
};
case ALL_PRODUCT_FAIL:
return {
loading: false,
error: action.payload,
};
case CLEAR_ERRORS:
return {
...state,
error: null,
};
default:
return state;
}
};
<b>action</b>
export const getProduct = () => async (dispatch) => {
try {
dispatch({ type: ALL_PRODUCT_REQUEST });
const { data } = await axios.get("/api/v1/products");
dispatch({
type: ALL_PRODUCT_SUCCESS,
payload: data,
});
} catch (error) {
dispatch({
type: ALL_PRODUCT_FAIL,
payload: error.response.data.message,
});
}
};
<b>store file</b>
import { createStore, combineReducers, applyMiddleware } from "redux";
import thunk from "redux-thunk";
import { composeWithDevTools } from "redux-devtools-extension";
import {
productReducer,
productDetailsReducer,
} from "./reducers/productReducer";
const reducer = combineReducers({
products: productReducer,
productDetails: productDetailsReducer,
});
let initialState = {};
const middleware = [thunk];
const store = createStore(
reducer,
initialState,
composeWithDevTools(applyMiddleware(...middleware))
);
export default store;
Not able to see this function works alert. error(error).when I console this error it shows undefined. I want to show an alert "Product Not Found" when getting an error.
Not sure what is your redux structure is, but it seems like you are extracting out your state from the redux store incorrectly. So maybe try to replace this
const { loading, error, products } = useSelector((state) => state.products);
with
const { loading, error, products } = useSelector((state) => {
return {
loading: state.loading,
products: state.products,
error:state.error
});
EDIT:
State present in the useSelector is your root reducer which means it contains all the reducers in it, just for example, your product reducer, your cart reducer, user reducer and so on. Since, productReducer is being used in your root reducer, you should first access the productReducer and then its properties which should look like below (adjust the variables as per your variable names)
const { loading, error, products } = useSelector((state) => {
return {
loading: state.products.loading,
products: state.products.products,
error:state.products.error
});
This is because useSelector actually accesses data from the state (managed by Redux) on the frontend. This is data after fetching from the API so this is purely the data part from the API and not error or loading
Here, you have destructured fields of whatever gets returned when you try to access the products slice of your state. Which will not have fields like loading or error but an array of product objects.
To fix this, you should store error and loading responses from your API in your Redux state in a structured and only then can you access them in each of your React components accessing this state.
eg code :
const { loading, error, products } = useSelector((state) => {
// Curly braces with return should not be on next line
return {
loading: state.loading,
products: state.products,
error:state.error
});
Also, I would suggest using packages like react-query to address such use cases well
Alternative
Write your own hook for handling such async tasks
Code for hook
import { useCallback, useEffect, useState } from "react"
export default function useAsync(callback, dependencies = []) {
const [loading, setLoading] = useState(true)
const [error, setError] = useState()
const [value, setValue] = useState()
// Simply manage 3 different states and update them as per the results of a Promise's resolution
// Here, we define a callback
const callbackMemoized = useCallback(() => {
setLoading(true)
setError(undefined)
setValue(undefined)
callback()
// ON SUCCESS -> Set the data from promise as "value"
.then(setValue)
// ON FAILURE -> Set the err from promise as "error"
.catch(setError)
// Irresp of fail or success, loading must stop after promise has ran
.finally(() => setLoading(false))
// This function runs everytime some dependency changes
}, dependencies)
// To run the callback function each time it itself changes i.e. when its dependencies change
useEffect(() => {
callbackMemoized()
}, [callbackMemoized])
return { loading, error, value }
}
Using this hook in the component
import useAsync from "./useAsync"
export default function AsyncComponent() {
const { loading, error, value } = useAsync(() => {
// 3 states and their updated versions are returned while the promise is getting resolved
return new Promise((resolve, reject) => {
const success = false
setTimeout(() => {
success ? resolve("Hi") : reject("Error")
}, 1000)
})
})
return (
<div>
<div>Loading: {loading.toString()}</div>
<div>{error}</div>
<div>{value}</div>
</div>
)
}
I'm just starting in redux and I want to include it on my existing app. What I want to do is to store my login response for me to use the user details on other page.
LandingPage.js
import { useDispatch } from 'react-redux'
function LandingPage(){
const dispatch = useDispatch();
const authLogin = async()=>{
const response = await axios.get('/api',)
let responseValue = response.data.success
if(responseValue === true) {
const parsedData = JSON.parse(response.data.resp)
dispatch({
type: 'SAVE_AUTH',
payload: {
isLoggedIn: responseValue,
username: parsedData.data.user.userName,
token: parsedData.data.token
}
})
}
useEffect(() => {
authLogin();
}, [])
return (
<div>
<label>Authenticating....</label>
<Login1 /> //updated based on #LindaPaiste's answer
</div>
export default LandingPage;
MainLanding.js
import React from 'react'
import Login1 from './Login1'
function MainLanding(){
return(
<div>
<h1>User Login Details</h1>
<Login1 /> //Nothing hapens here
</div>
)
}
export default MainLanding;
Login1.js
import React from 'react'
import LoginDetailsx from './LoginDetailsx'
import { useSelector } from 'react-redux'
function Login1(){
const userLoginDetails = useSelector((state) => state.loginDetails)
console.log('userLoginDetails',userLoginDetails)
return(
<div>
<h2>Login Details</h2>
<LoginDetailsx isLogin={userLoginDetails.isLoggedIn} username={userLoginDetails.username} token={userLoginDetails.token}/>
})}
</div>
)}
export default Login1;
loginDetailsReducer.js
const initialState = [
{
isLoggedIn: false,
}];
const loginDetailsReducer = (state = initialState, action) => {
const { type, payload } = action;
console.log('typex',type)
console.log('payloadx',payload)
switch(type){
case "SAVE_AUTH":
alert('dasdasd')
return payload;
case "LOGOUT_AUTH":
return initialState
default:
return state;
}
}
export default loginDetailsReducer;
rootReducer.js
import { combineReducers } from 'redux'
import loginDetailsReducer from '../reduxReducers/loginDetailsReducer'
const rootReducer = combineReducers({
loginDetails: loginDetailsReducer
});
export default rootReducer;
store.js
import { createStore } from 'redux'
import rootReducer from '../reduxReducers/rootReducer'
const store = createStore(rootReducer);
export default store;
LoginDetailsx.js
import React from 'react'
function LoginDetailsx(props){
return(
<div>
<p>Details: isloggedin: {props.isloggedin}, username: {props.username}, token: {props.token}</p>
</div>
)
}
export default LoginDetailsx;
This is what I'm getting on MainLanding.js after successful login.
and this is what i'm getting on LandingPage.js console.log
State Shape
While not necessarily a problem, it really doesn't make sense that the loginDetails state should be an array. Only one user should be logged in at a time, so it should just be an object with the user details. That makes your reducer extremely simple (as always Redux Toolkit can make it even simpler).
You'll want to add a logout case too. isLoggedIn should be a boolean instead of a string. I personally think that undefined makes more sense than '' for username and token when there is no logged in user but that's up to you.
const initialState = {
isLoggedIn: false,
// no username or token when logged out
};
const loginDetailsReducer = (state = initialState, action) => {
const { type, payload } = action;
switch(type) {
case "SAVE_AUTH":
// replace the state with the action payload
return payload;
case "LOGOUT_AUTH":
// revert to initial state
return initialState;
default:
return state;
}
}
export default loginDetailsReducer;
Logging In
I was going to say that asynchronous actions like API calls need to be done inside a useEffect hook in the component. You can use an empty dependency array to run the effect once when the component is mounted.
useEffect(() => {
authLogin();
}, []);
But now I'm looking at your image and it seems like you are executing the action in response to a button click, so that's fine too.
axios handles JSON parsing so you should not need to use JSON.parse() (unless your API is returning strange data).
function MainLanding() {
const isLoggedIn = useSelector((state) => state.loginDetails.isLoggedIn);
// access dispatch function
const dispatch = useDispatch();
// define the function to log in
const authLogin = async () => {
const response = await axios.get("/api");
const data = response.data;
if (data.success === true) {
dispatch({
type: "SAVE_AUTH",
payload: {
isLoggedIn: true,
username: data.resp.user.userName,
token: data.resp.data.token
}
});
}
};
return (
<div>
{isLoggedIn ? (
<>
<h1>User Login Details</h1>
<Login1 />
</>
) : (
<button onClick={authLogin}>Log In</button>
)}
</div>
);
}
I want to navigate to App screen or Auth screen, depending on the isUser prop after fetching it from the server and updating the redux store.
My first component AuthLoading.js which looks like this:
const AuthLoading = (props) => {
const isUser = useSelector((state) => state.authReducer.isUserExists);
const dispatch = useDispatch();
const fetchData = async () => {
const token = await TokensHandler.getTokenFromDevice();
dispatch(isTokenExists(token));
props.navigation.navigate(isUser ? "App" : "Auth");
};
useEffect(() => {
fetchData();
}, []);
My authActions.js looks like this:
export const isTokenExists = (token) => {
return (dispatch) => {
return HttpClient.get(ApiConfig.IDENTITY_PORT, "api/identity", {
userId: token,
}).then((response) => {
console.log(response);
dispatch({
type: IS_USER_EXISTS,
payload: response,
});
});
};
};
My authReducer.js looks like this:
const authReducer = (state = initialState, action) => {
switch (action.type) {
case IS_USER_EXISTS:
return {
...state,
isUserExists: action.payload,
};
default:
return state;
}
};
And the store:
import { createStore, applyMiddleware } from "redux";
import thunk from "redux-thunk";
import rootReducer from "../reducers";
const configureStore = () => {
return createStore(rootReducer, applyMiddleware(thunk));
};
export default configureStore;
Unfortunately, the code inside AuthLoading.js isn't asynchronous, and isn't waiting for the updated isUser value and running the next line without it.
I've tried to .then after dispatch which still doesn't work.
I've tried changing async states and still couldn't find the problem.
I have no idea how to fix this.
Thanks for the help.
You can use another useEffect hook that runs when isUser changes and navigate inside it.
This useEffect runs once the component mounts and every time isUser changes, so someCondition can be anything that determines whether the navigation should happen or not.
useEffect(() => {
if(someCondition) {
props.navigation.navigate(isUser ? "App" : "Auth");
}
}, [isUser]); // add all required dependencies
So I just started learning hooks and I'm trying to use Redux with them. Ive been trying redux new api but now I can't seem to get the data that I want. When I do console.log() I see the promise getting resolved and inside the [[PromiseValue]] I see the data but how do I get it out of there into my Component.
const Main = ({props}) => {
const [cloth,setCloth]=useState([])
const items = useSelector((state) => state);
const dispatch = useDispatch()
let getItems = getAllItems(
() => dispatch({ type: 'GET_ITEMS' }),
[dispatch]
)
console.log(getItems)
here is the pic of my console.log()
first to use redux with async actions you need to use redux-thunk middleware:
npm i redux-thunk
and then use it like so:
import { createStore, applyMiddleware } from 'redux';
const store = createStore(rootReducer, applyMiddleware(thunk));
now for the actions here a simple example:
first action to fetch Items and then action to set Items in store:
//actions.js
const setItems = (payload)=>({type:SET_ITEMS,payload})
export const fetchItem = ()=>{
return dispatch =>{
axios.get("/items").then(response=>
dispatch(setItems(response.data))).catch(error=>throw error)
}
}
//reducer.js
const state = {items:[]}
export default function todosReducer(state = initialState, action) {
switch (action.type) {
case "SET_ITEMS":
return {
...state,
items: payload
};
default:
return state;
}
}
now from react component we fetch items on mount :
import React,{useEffect} from "react"
import {useDispatch,useSelector} from "react-redux"
import {fetchItems} from "action.js"
const Items = ()=>{
const items= useSelector(state=>state.items)
const dispatch = useDispatch()
useEffect(()=> dispatch(fetchItems()),[])
return items?items.map(item =><p key={item.name}>{item.name}</p>):<p>items not available</p>
hope this what you are looking for.