I'm trying to display products using the fetched axios result from reducer, but the useSelector value just won't change and is still empty even after dispatch. I have checked the axios result and the response has correct data. Does it have something to do with this line on redux documentation?
With useSelector(), returning a new object every time will always force a re-render by default.
reducer
import axios from "axios";
export const products = (state = [], action) => {
switch (action.type) {
case "FETCH_PRODUCTS": {
const uri = "/products";
axios.get(uri).then(function (response) {
if (response.status == 200) {
console.log(response.data.products); // ===> correct new value
return { state: response.data.products };
}
});
}
App.js
import React, { useEffect } from "react";
import { shallowEqual, useSelector, useDispatch } from "react-redux";
import "../css/App.css";
import { Products, Navbar, Cart } from "../components";
function App() {
const dispatch = useDispatch();
const products = useSelector((state) => state.products, shallowEqual);
const cart = useSelector((state) => state.cart, shallowEqual);
useEffect(() => {
dispatch({
type: "FETCH_PRODUCTS",
});
console.log(products); // ===> still empty array
}, []);
return (
<div className="App">
<Navbar />
<Cart cart={cart} />
<Products products={products} />
</div>
);
}
export default App;
You should first create your action
import axios from 'axios';
export const fetchProducts = () => async (dispatch) => {
try {
const { data } = await axios.get('...');
dispatch({ type: "FETCH_PRODUCTS", payload: data.result });
} catch (error) {
console.log(error);
}
};
Then, Use dispatch and action together
import { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { fetchProducts } from './actions';
const getSelectors = state => ({ cart: state.cart, products: state.products });
const App = () => {
const dispatch = useDispatch();
const {cart, products} = useSelector(getSelectors);
useEffect(() => {
dispatch(fetchProducts());
}, []);
return (
<div className="App">
<Navbar />
<Cart cart={cart} />
<Products products={products} />
</div>
);
};
Related
I am new to this so please bear with me, I am trying to pass the products & the empty cart from (Context.js) to the (cartState) in apps.js file, through useReducer as intructed in minute 20 of this guide, the only difference is that he is using a faker to generate the Api and I am pulling data with Axios in a useEffect from Api with 13 products
I tried in many ways to receive the items inside the products array but always receiving an empty array in console log of the (carState) in App.js File, all help is welcome appreciated ! Github Repo
Context.js
import { createContext, useState, useEffect, useReducer, useContext } from 'react';
import { cartReducer } from './Reducers';
import { ApiServer } from '../services/Api/index'
const Cart = createContext();
const Context = ({ children }) => {
const [products, setProducts] = useState([]);
useEffect(() => {
ApiServer.get('/products/')
.then(response => {
if (response.data != null) {
setProducts(response.data);
} else {
console.log('O Array esta vazio')
}
})
.catch(error => {
console.log(error);
});
}, []);
console.log(products)
const [state, dispatch] = useReducer(cartReducer, {
products: products,
cart: [],
});
return <Cart.Provider value={{ state, dispatch }}> {children}</Cart.Provider>;
}
export default Context
export const CartState = () => {
return useContext(Cart);
};
Reducer.js
export const cartReducer = (state, action) => {
switch (action.type) {
default:
return state;
}
};
App.js
import React from 'react';
import Router from './routes';
import GlobalStyle from './assets/styles/globalStyles';
import ScrollToTop from './components/ScrollToTop';
import { BrowserRouter } from 'react-router-dom';
import { ContentProvider } from './useContext';
import { CartState } from './contexts/Context';
function App() {
const { state } = CartState();
console.log(state);
return (
<BrowserRouter>
<ContentProvider>
<GlobalStyle />
<Router />
<ScrollToTop />
</ContentProvider>
</BrowserRouter>
);
}
export default App;
What you are trying to do here will not work. The initial state of the reducer is just that -- the INITIAL state. It will use the value of initialState from the very first render, before your items has been updated with the fetch results. Changes to the variable initialState will not effect the reducer after it has already been created. You have to dispatch an action to store the items in your reducer. source
Solution:
As you are using useReducer() for state handling you don't need to use useState()
Write your useReducer like:
const [state, dispatch] = useReducer(reducer, {
products: [],
cart: []
});
Reducer function will be like:
export const reducer = (state, action) => {
switch (action.type) {
case "SET_PRODUCTS_DATA":
return { ...state, products: action.payload };
default:
return state;
}
}
Set product data from useEffect using dispatch like:
useEffect(() => {
ApiServer.get("/products/")
.then((response) => {
if (response.data != null) {
dispatch({ type: "SET_PRODUCTS_DATA", payload: response.data });
} else {
console.log("O Array esta vazio");
}
})
.catch((error) => {
console.log(error);
});
}, []);
It should work perfectly. Demo codesandbox Link
I'm attempting to get information about NBA teams using Redux, then put that data into a table. Right now I'm able to get the data from the API, but I'm struggling to figure out the correct syntax to display a table with the data I collect using Redux (I usually use getContext and am trying to familiarize myself with Redux more).
This is my App.js
import React, { useEffect } from "react";
import { useSelector, useDispatch } from "react-redux";
import { getTeams, getTable } from "./features/teams/teamsSlice";
import { Button } from "#mui/material";
import rsLogo from "./logo-with-name.png";
import "./App.css";
function App() {
const dispatch = useDispatch();
const { teams } = useSelector((state) => state.teams);
const { table } = useSelector((state) => state.table);
useEffect(() => {
dispatch(getTeams());
}, [dispatch]);
console.log(teams);
console.log(table);
return (
<div className="App">
<header className="App-header">
<img src={rsLogo} className="App-logo" alt="logo" />
</header>
<main>
<Button
variant="contained"
target="_blank"
onClick={() => dispatch(getTable(teams))}
size="large"
sx={{ m: 2, bgcolor: "#00003C" }}
disableElevation
>
Show Teams
</Button>
{table}
</main>
</div>
);
}
export default App;
This is my store.js
import { configureStore } from "#reduxjs/toolkit";
import teamsReducer from "../features/teams/teamsSlice";
const store = configureStore({
reducer: {
teams: teamsReducer,
},
});
export default store;
And this is teamsSlice.js
import { createSlice, createAsyncThunk } from "#reduxjs/toolkit";
export const getTeams = createAsyncThunk(
"teams/getTeams",
async (dispatch, getState) => {
return await fetch("https://www.balldontlie.io/api/v1/players").then(
(res) => {
return res.json();
}
);
}
);
const teamsSlice = createSlice({
name: "team",
initialState: {
teams: [],
table: "",
status: null,
},
reducers: {
getTable(teams, action) {
console.log("teams is ", teams);
console.log("action is ", action);
return <div>test</div>;
},
},
extraReducers: {
[getTeams.pending]: (state, action) => {
state.status = "loading";
},
[getTeams.fulfilled]: (state, action) => {
state.status = "success";
state.teams = action.payload;
},
[getTeams.rejected]: (state, action) => {
state.status = "failed";
},
},
});
const { actions, reducer } = teamsSlice;
export const { getTable } = actions;
export default teamsSlice.reducer;
I haven't yet created the actual table, I'm just trying to get the syntax correct for being able to click a button and return a table based on data from the store.
Could anyone tell me what I'm doing wrong?
Since you are using redux and your request is not in the file it is important to keep an eye on changes to the object you need the information for, this is one way to do it:
import React, { useState, useEffect } from "react";
import { useSelector, useDispatch } from "react-redux";
import { getTeams, getTable } from "./features/teams/teamsSlice";
import { Button } from "#mui/material";
import rsLogo from "./logo-with-name.png";
import "./App.css";
function App() {
const dispatch = useDispatch();
const { teams } = useSelector((state) => state.teams);
const { table } = useSelector((state) => state.table);
const [dataTable, setdataTable] = useState([]);
useEffect(() => {
dispatch(getTeams());
}, [dispatch]);
useEffect(() => {
if(table) setdataTable(table);
}, [table])
return (
<div className="App">
<header className="App-header">
<img src={rsLogo} className="App-logo" alt="logo" />
</header>
<main>
<Button
variant="contained"
target="_blank"
onClick={() => dispatch(getTable(teams))}
size="large"
sx={{ m: 2, bgcolor: "#00003C" }}
disableElevation
>
Show Teams
</Button>
{dataTable}
</main>
</div>
);
}
export default App;
with this, when the effect detects a change in that object it will put it in the state when it exists
i've been working on a project trying to learn redux with react. But there is an error and i don't know exactly how to fix it. Files/codes in down if you need more information about how store works.
store/index.js
import { createStore, applyMiddleware, compose } from "redux";
import rootReducer from "./reducers";
import thunk from 'redux-thunk'
const middlewares = [thunk]
const store = createStore(rootReducer, compose(applyMiddleware(...middlewares), window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()))
export default store;
actions/index.js
import axios from "axios"
export const increment = (payload) => {
return {
type: 'INCREMENT',
payload: payload
}
}
export const decrement = () => {
return {
type: 'DECREMENT'
}
}
export const fetch = () => {
return async (dispatch) => {
axios.get('https://jsonplaceholder.typicode.com/posts/')
.then(data => dispatch({type: 'FETCH', payload: data.data}))
}
}
store/todos.js
const todos = (state = [], action) => {
switch(action.type){
case 'FETCH':
return Object.assign(state, action.payload);
default:
return state;
}
}
export default todos;
App.js
import logo from './logo.svg';
import './App.css';
import {useSelector, useDispatch, connect} from 'react-redux'
import {increment, decrement, fetch} from './store/actions/'
import { GeistProvider, CssBaseline } from '#geist-ui/react';
function App(props) {
const count = useSelector((state) => state.counter)
const todos = useSelector((state) => state.todos)
const dispatch = useDispatch()
return (
<div className="App">
<header className="App-header">
<h1>Count is {count}</h1>
<button onClick={() => dispatch(increment(3))}>+</button>
<button onClick={() => dispatch(decrement())}>-</button>
<button onClick={() => dispatch(fetch())}>FETCH</button>
{todos.length ? todos[0].title : <h1>Not fetched.</h1>}
</header>
</div>
);
}
export default App;
This is the codes in project. Let me know if you guys need more information about anything. Thanks for help!
You are wrong at return Object.assign(state, action.payload);. It's mutated state so redux can't detect state change. You should read this https://redux.js.org/understanding/thinking-in-redux/three-principles#changes-are-made-with-pure-functions
You can change to this
return Object.assign({}, state, action.payload);
or this
return { ...state, ...action.payload }
Getting a weird error where 'map' is undefined. I'm not sure if my functions are firing at the wrong time and that's resulting in no data being received.
I'm adding Redux into my simple little application that just pulls data from an API and displays it. It's a list of a bunch of Heroes. Like I said before, I think that the error is coming from different times in the ansyc API call and when Redux is firing. But then again I'm a novice so any help is much appreciated.
import React, {useEffect} from 'react'
import { connect } from 'react-redux'
import { fetchHeroes } from '../actions/heroesActions'
import { Hero } from '../components/Hero'
const HeroesPage = ({ dispatch, loading, heroes, hasErrors }) => {
useEffect(() => {
dispatch(fetchHeroes())
}, [dispatch])
const renderHeroes = () => {
if (loading) return <p>Loading posts...</p>
if (hasErrors) return <p>Unable to display posts.</p>
return heroes.map(hero => <Hero key={hero.id} hero={hero} />)
}
return (
<section>
<h1>Heroes</h1>
{renderHeroes()}
</section>
)
}
// Map Redux state to React component props
const mapStateToProps = state => ({
loading: state.heroes.loading,
heroes: state.heroes.heroes,
hasErrors: state.heroes.hasErrors,
})
export default connect(mapStateToProps)(HeroesPage)
export const GET_HEROES = 'GET HEROES'
export const GET_HEROES_SUCCESS = 'GET_HEROES_SUCCESS'
export const GET_HEROES_FAILURE = 'GET_HEROES_FAILURE'
export const getHeroes = () => ({
type: GET_HEROES,
})
export const getHeroesSuccess = heroes => ({
type: GET_HEROES_SUCCESS,
payload: heroes,
})
export const getHeroesFailure = () => ({
type: GET_HEROES_FAILURE,
})
export function fetchHeroes() {
return async dispatch => {
dispatch(getHeroes())
try {
const response = await fetch('https://api.opendota.com/api/heroStats')
console.log(response)
const data = await response.json()
dispatch(getHeroesSuccess(data))
} catch (error) {
dispatch(getHeroesFailure())
}
}
}
index.js where I created the store
// External imports
import React from 'react'
import { render } from 'react-dom'
import { createStore, applyMiddleware } from 'redux'
import { Provider } from 'react-redux'
import thunk from 'redux-thunk'
import { composeWithDevTools } from 'redux-devtools-extension'
// Local imports
import App from './App'
import rootReducer from './reducers'
const store = createStore(rootReducer, composeWithDevTools(applyMiddleware(thunk)))
render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)
import React, {useEffect} from 'react'
import { useSelector } from 'react-redux'
import { fetchHeroes } from '../actions/heroesActions'
import { Hero } from '../components/Hero'
const HeroesPage = () => {
const data = useSelector(state => state.heroes);
useEffect(() => {
fetchHeroes();
}, [])
const renderHeroes = () => {
if (data.loading) return <p>Loading posts...</p>
if (data.hasErrors) return <p>Unable to display posts.</p>
return data.heroes.map(hero => <Hero key={hero.id} hero={hero} />)
}
return (
<section>
<h1>Heroes</h1>
{renderHeroes()}
</section>
)
}
export default HeroesPage
action file
// import store from your createStore file and access dispatch from it
dispatch = store.dispatch
export const GET_HEROES = 'GET HEROES'
export const GET_HEROES_SUCCESS = 'GET_HEROES_SUCCESS'
export const GET_HEROES_FAILURE = 'GET_HEROES_FAILURE'
export const getHeroes = () => ({
type: GET_HEROES,
})
export const getHeroesSuccess = heroes => ({
type: GET_HEROES_SUCCESS,
payload: heroes,
})
export const getHeroesFailure = () => ({
type: GET_HEROES_FAILURE,
})
export const fetchHeroes = () => {
dispatch(getHeroes())
try {
const response = await fetch('https://api.opendota.com/api/heroStats')
console.log(response)
const data = await response.json()
dispatch(getHeroesSuccess(data))
} catch (error) {
dispatch(getHeroesFailure())
}
}
reducer file
import * as actions from '../actions/heroesActions'
export const initialState = {
heroes: [],
loading: false,
hasErrors: false,
}
export default function heroesReducer(state = initialState, action) {
switch (action.type) {
case actions.GET_HEROES:
return { ...state, loading: true }
case actions.GET_HEROES_SUCCESS:
return { heroes: action.payload, loading: false, hasErrors: false }
case actions.GET_HEROES_FAILURE:
return { ...state, loading: false, hasErrors: true }
default:
return state
}
}
Ive been trying to do this with react hooks and the useSelector/useDispatch. What happens is, I am able to see the data and state change in the Redux DevTools however, when logging to the console, I either get an empty array or undefined. I am also not able to render the data to the screen expectedly.
Posts Component
import React, {useState, useEffect} from 'react'
import PropTypes from 'prop-types'
import {useSelector, useDispatch} from 'react-redux'
import {getPosts} from '../actions/postActions'
const Posts = props =>{
const dispatch = useDispatch();
const postData = useSelector(state=> state.items, []) || []; //memoization?
const [items, setItems] = useState(postData)
console.log(postData);
useEffect(() => {
dispatch(getPosts());
}, []);
return(
<h1>{postData[0]}</h1>
)
}
export default Posts
ACTIONS
import {GET_POSTS, NEW_POSTS} from '../actions/types'
export const getPosts =()=> dispatch =>{
//fetch
console.log('fetching')
const url = 'https://jsonplaceholder.typicode.com/posts/'
fetch(url)
.then(res => res.json())
.then(posts=> dispatch({type: GET_POSTS, payload: posts}))
}
reduxDevTools image
I think the problem is coming from this line:
const postData = useSelector(state=> state.items, []) || [];
If you want postData to initially be an array, it's best to set it as an array in your reducer.
Working example (click Posts tab to make API call):
actions/postActions.js
import api from "../utils/api";
import * as types from "../types";
export const getPosts = () => async dispatch => {
try {
dispatch({ type: types.POSTS_FETCH });
const res = await api.get("posts");
dispatch({
type: types.POSTS_SET,
payload: res.data
});
} catch (err) {
dispatch({
type: types.POSTS_FAILED_FETCH,
payload: err.toString()
});
}
};
reducers/postsReducer.js
import * as types from "../types";
const initialState = {
data: [],
error: "",
isLoading: true
};
export default (state = initialState, { type, payload }) => {
switch (type) {
case types.POSTS_FETCH:
return initialState;
case types.POSTS_SET:
return {
...state,
data: payload,
error: "",
isLoading: false
};
case types.POSTS_FAILED_FETCH:
return {
...state,
error: payload,
isLoading: false
};
default:
return state;
}
};
containers/FetchPostsHooks/index.js
import React, { useEffect } from "react";
import { useSelector, useDispatch } from "react-redux";
import { getPosts } from "../../actions/postActions";
import Spinner from "../../components/Spinner";
import DisplayPosts from "../../components/DisplayPosts";
const Posts = () => {
const dispatch = useDispatch();
const { isLoading, data, error } = useSelector(state => state.posts, []);
useEffect(() => {
dispatch(getPosts());
}, [dispatch]);
return isLoading ? (
<Spinner />
) : error ? (
<p>{error}</p>
) : (
<DisplayPosts data={data} />
);
};
export default Posts;