I'm building the MERN eCommerce app, and I'm facing a weird UI bug with pulling data from Redux Store.
After switching pages from one product page to another, there is the old product image shown and then updated.
As you can see: https://imgur.com/iU9TxJr
There you can see code for reducer:
export const productDetailsReducer = (
state = { product: { reviews: [] } },
action
) => {
switch (action.type) {
case PRODUCT_DETAILS_REQUEST:
return { loading: true, ...state };
case PRODUCT_DETAILS_SUCCESS:
return { loading: false, product: action.payload };
case PRODUCT_DETAILS_FAIL:
return { loading: false, error: action.payload };
default:
return state;
}
};
And also code for action:
export const listProductDetails = (id) => async (dispatch) => {
try {
dispatch({ type: PRODUCT_DETAILS_REQUEST });
const { data } = await axios.get(`/api/products/${id}`);
dispatch({
type: PRODUCT_DETAILS_SUCCESS,
payload: data,
});
} catch (error) {
dispatch({
type: PRODUCT_DETAILS_FAIL,
payload:
error.response && error.response.data.message
? error.response.data.message
: error.message,
});
}
};
And lastly, there is component's code where I bringing redux store into the page
const ProductPage = ({ match }) => {
const dispatch = useDispatch();
const productDetails = useSelector((state) => state.productDetails);
const { loading, error, product } = productDetails;
useEffect(() => {
dispatch(listProductDetails(match.params.id));
}, [dispatch, match]);
console.log(product);
return (
<>
{loading ? (
<Loader />
) : error ? (
<Message variant="danger">{error}</Message>
) : (
<Row>
<Col md={8}>
<Image src={product.image} alt={product.name} fluid />
</Col>
...rest of component's code....
I know probably would be the best to not even use redux for a single product, but I'm using this project for practice, and I'm kinda stuck at this one.
Thanks to everyone!
What you need to do is clear the selected product when you leave the detail page. You can do that using the return function of useEffect. So probably something like:
useEffect(() => {
dispatch(listProductDetails(match.params.id));
return () => {
dispatch(clearSelectedProduct());
}
}, [dispatch, match]);
And add corresponding action and reducer changes..
case CLEAR_SELECTED_PRODUCT:
return { loading: false, product: { reviews: [] } };
This way, when you arrive on the product detail page, the previous product is always cleared.
Related
I am getting the data from the api as expected but the problem here is I am getting it in a 3rd attempt which is causing the error in my application when there's no data to show.
I am testing it printing on a console but it's the same error. As Soon As I Refresh My Page The Error Comes Flooding In The Console
Reducer
export const productDetailsReducer = (state = { products: {} }, action) => {
switch (action.type) {
case PRODUCT_DETAILS_REQUEST:
return {
...state,
loading: true,
};
case PRODUCT_DETAILS_SUCCESS:
return {
loading: false,
product: action.payload,
};
case PRODUCT_DETAILS_FAIL:
return {
...state,
error: action.payload,
};
case CLEAR_ERRORS:
return {
...state,
error: null,
};
default:
return state;
}
};
Component
const ProductDetails = () => {
const dispatch = useDispatch();
const alert = useAlert();
const { id } = useParams();
const { product, loading, error } = useSelector(
(state) => state.productDetails
);
useEffect(() => {
dispatch(getProductDetails(id));
if (error) {
alert.error(error);
dispatch(clearErrors());
}
}, [dispatch, id, alert, error]);
console.log(product);
Action
export const getProductDetails = (id) => async (dispatch) => {
try {
dispatch({ type: PRODUCT_DETAILS_REQUEST });
const { data } = await axios.get(`/api/v1/product/${id}`);
dispatch({
type: PRODUCT_DETAILS_SUCCESS,
payload: data.product,
});
} catch (error) {
dispatch({
type: PRODUCT_DETAILS_FAIL,
payload: error.response.data.message,
});
}
};
App.js
function App() {
return (
<Router>
<div className="App">
<Header />
<div className="container container-fluid">
<Routes>
<Route path="/" element={<Home />} exact />
<Route path="/product/:id" element={<ProductDetails />} />
</Routes>
</div>
<Footer />
</div>
</Router>
);
}
You haven't set the loading in the initial state I think that's why
export const productDetailsReducer = (state = { products: {} }, action) => {
you are trying to access the loading state before you set the value of the state that's why you are getting the error
you should set the loading and error in the initial state with the default values
const initialState = {products: {}, loading: true, error: null}
and then pass it to the reducer.
export const productDetailsReducer = (state = initialState, action) => {
and change the product into products
hopefully, this will fix your issue.
I have checked your code and I think the problem is with your initial state in
export const productDetailsReducer = (state = { products: {} }, action) =>
try changing state={products:{}} with state={product: {}}
I'm trying to fetch data through endpoint from Django Rest Framework
endpoint is :
/api/v1/categories/nested/{id}/
Problem is when I'm requesting with id, Django server show this error :
ValueError: Field 'id' expected a number but got 'undefined'.
[07/Feb/2022 15:53:01] "GET /api/v1/categories/nested/undefined/ HTTP/1.1" 500 162581
As this suggest I'm unable to Pass id correctly,
So need littl help to fix that
I'm using actions > reducer > store > component approach using react redux
action.js
export const listCategoryDetails = (id) => async (dispatch) => {
try {
dispatch({ type: CATEGORY_DETAIL_REQUEST });
const { data } = await axios.get(`/api/v1/categories/nested/${id}`); // Purpose to show nested brands[]
dispatch({
type: CATEGORY_DETAIL_SUCCESS,
payload: data,
});
} catch (error) {
dispatch({
type: CATEGORY_DETAIL_FAIL,
payload:
error.response && error.response.data.detail
? error.response.data.detail
: error.message,
});
}
};
reducer.js
export const categoryDetailsReducer = (
state = { category: { } },
action
) => {
switch (action.type) {
case CATEGORY_DETAIL_REQUEST:
return { loading: true, ...state };
case CATEGORY_DETAIL_SUCCESS:
return { loading: false, category: action.payload };
case CATEGORY_DETAIL_FAIL:
return { loading: false, error: action.payload };
default:
return state;
}
};
store.js
const reducer = combineReducers({
categoryDetail: categoryDetailsReducer,
});
component
function CategoryDetail({ match, history }) {
// const { id } = useParams();
// console.log(id);
const dispatch = useDispatch();
const categoryList = useSelector((state) => state.categoryList);
const { loading, error, categories , page, pages} = categoryList;
useEffect(() => {
dispatch(listCategoryDetails());
}, [dispatch, match]);
return <div>
{categories.map(category => (
<Col key={category.id} sm={12} md={8} lg={4} xl={3} >
<h1><strong>{category.title}</strong></h1>))}
</div>;
}
export default CategoryDetail;
const id = ...
and pass it to dispatch function dispatch(listCategoryDetails(id))
Before
// const { id } = useParams();
// console.log(id);
const dispatch = useDispatch();
const categoryList = useSelector((state) => state.categoryList);
const { loading, error, categories , page, pages} = categoryList;
useEffect(() => {
dispatch(listCategoryDetails());
}, [dispatch, match]);
After
const { id } = useParams();
console.log(id);
const dispatch = useDispatch();
const categoryList = useSelector((state) => state.categoryList);
const { loading, error, categories , page, pages} = categoryList;
useEffect(() => {
dispatch(listCategoryDetails(id));
}, [dispatch, match]);
Inside UseEffect You Are Not Passing id Variable So its Saying Id Is Undefined
I added like and unlike buttons to my react app. I'm using redux to manage the state and storing the data in firebase realtime-database. The buttons are working as they should but I need to reload the page to show the post has been liked/unliked, it is not re-rendering on its own. I tried using both forceUpdate and setState but both didn't work.
postLiked = (id) => {
this.props.onLikePost(this.props.user, id)
this.forceUpdate()
}
postUnliked = (id, unlikeID) => {
this.props.onUnlikePost(id, unlikeID)
}
render() {
{this.props.data.map((res) => {
const liked = [];
for(let key in res.LikedBy){
liked.push({
...res.LikedBy[key],
id: key
});
}
let boolButton = false;
if(liked.filter((val) => {
if(val.username === this.props.user) {
boolButton = true
}
}))
return(
<div>
<div className="bottomButtons">
{boolButton ? <Button className="btn btn-primary likeDislike"
id="likeButton"
onClick={() => this.postUnliked(res.id, liked.find((val) => {
if(val.username === this.props.user){
return val.id;
}
}))}
>
<FontAwesomeIcon icon={faThumbsUp} style={{width:"13.5px", color:"white"}}/>
</Button> : <Button className="btn btn-light likeDislike"
id="likeButton"
onClick={() => this.postLiked(res.id)}
>
<FontAwesomeIcon icon={faThumbsUp} style={{width:"13.5px"}}/>
</Button>
}
These are the action functions
export const likePost = (username, postId) => {
const req = {
username,
postId
}
return (dispatch) => {
axios.post('/Data/' + postId + '/LikedBy.json', req)
.then((res) => {
dispatch({
type: actionTypes.Like_Post,
payload: res
})
})
}
}
export const unlikePost = (id, unlikeId) => {
return (dispatch) => {
axios.delete('/Data/' + id + '/LikedBy/' + unlikeId.id + '.json')
.then((res) => {
dispatch({
type: actionTypes.Unlike_Post
})
}).catch((error) => {
console.log(error)
})
}
}
And this is the reducer function
const initialState = {
Data: []
}
const reducer = (state = initialState, action) => {
switch(action.type){
case actionTypes.Like_Post:
return {
...state,
Data: state.Data.map((post) => post.id === action.payload.postId
? {...post, LikedBy: post.LikedBy.concat(action.payload.username)}:post),
loading: false,
}
case actionTypes.Unlike_Post:
return {
...state,
Data: state.Data,
loading: false,
}
EDIT
I tried other methods but nothing is working. The issue is with the reducer and I am not correctly updating the state. I tried updating the LikedBy field but I only get an error.
Tried this approach but I got an error saying res.map is not a function
case actionTypes.Like_Post:
return {
...state,
Data: state.Data.forEach((res) => res.map((q) => {
if(q.id === action.payload.postId) {
q.LikedBy.concat(action.payload.username)
}
return q
})
)
}
You shouldn't be re-loading the page to upload this state really (don't listen to people who tell you to do it); this is one of the many problems that React was designed to solve. The reason you are having an issue is because your component isn't SEEING a change to its data therefor a re-render is not being triggered, this is most likely because you aren't passing the correct prop into your component.
Your redux reducer is referring to Data but locally in your component you are using this.props.data try making sure you are actually passing your data reducer properly into the component.
I have an issue with redux and probably useEffect(I am not sure where my mistake is). I am trying to get information from PokeAPI and store information in the redux state. The problem is that the information about pokemons don't include pokemon types(fire, water, etc.), to solve this I am sending requests to fetch those types from a different endpoint and I want to include these types of specific pokemon to redux state.
1-redux state without types of pokemons
2-redux state with types of pokemons
My goal is to have a state like in the second picture with types. But when I refresh the page, I only acquire the first picture(actions aren't dispatching). When I change something in my code and save it, I get types as well. I suspect that my problem is in the useEffect, but I couldn't find a solution without creating some nasty loops.
Note: Page parameter in fetchData coming from PokeAPI, it basically returns 15 pokemon for every page.(For now I am just experimenting on the first page)
This is my first question in stackoverflow, I already searched for similar questions but those were dealing with different aspects, so I decided to ask myself.
PokemonList.js --> this is where I need those types
import React, { useEffect } from 'react';
import { ListGroup, ListGroupItem } from "react-bootstrap";
import { useDispatch, useSelector } from 'react-redux';
import _ from "lodash";
import { GetPokemonList, GetSpecificPokemon } from '../redux/actions/PokemonAction';
import { Button } from 'react-bootstrap';
const PokemonList = () => {
const dispatch = useDispatch();
const pokemonList = useSelector(state => state.PokemonList);
useEffect(() => {
const fetchData = (page = 1) => {
dispatch(GetPokemonList(page));
}
fetchData();
}, [dispatch]);
useEffect(() => {
const fetchTypes = () => {
pokemonList.data.forEach(pokemon => {
dispatch(GetSpecificPokemon(pokemon.name));
});
}
fetchTypes();
}, [dispatch]);
const showData = () => {
if (!_.isEmpty(pokemonList.data)) {
return (
<div className="pokemon-list-wrapper">
{pokemonList.data.map((pokemon, index) => {
return (
<div className="pokemon-list-element" key={index}>
<ListGroup>
<ListGroupItem action href={`/pokemon/${pokemon.name}`} variant="success">{pokemon.name}
<Button style={{ float: "right" }}>Test</Button>
</ListGroupItem>
</ListGroup>
</div>
)
})}
</div>
)
}
if (pokemonList.loading) {
return <p>Loading...</p>
}
if (pokemonList.errorMessage !== "") {
return <p>{pokemonList.errorMessage}</p>
}
};
return (
<div>
{showData()}
</div>
)
};
export default PokemonList;
PokemonAction.js
import axios from "axios"
export const GetPokemonList = (page) => async (dispatch) => {
try {
dispatch({
type: "POKEMON_LIST_LOADING"
});
const perPage = 15;
const offset = (page * perPage) - perPage;
const res = await axios.get(`https://pokeapi.co/api/v2/pokemon?limit=${perPage}&offset=${offset}`);
dispatch({
type: "POKEMON_LIST_SUCCESS",
payload: res.data
});
} catch (e) {
dispatch({
type: "POKEMON_LIST_FAIL"
});
}
}
export const GetSpecificPokemon = (name) => async (dispatch) => {
try {
const res = await axios.get(`https://pokeapi.co/api/v2/pokemon/${name}`);
dispatch({
type: "SPECIFIC_POKEMON_SUCCESS",
payload: res.data
});
} catch (e) {
dispatch({
type: "SPECIFIC_POKEMON_FAIL"
});
}
}
PokemonListReducer.js
const initialState = {
data: [],
loading: false,
errorMessage: "",
count: 0
};
const PokemonListReducer = (state = initialState, action) => {
switch (action.type) {
case "POKEMON_LIST_LOADING":
return {
...state,
loading: true,
errorMessage: ""
};
case "POKEMON_LIST_FAIL":
return {
...state,
loading: false,
errorMessage: "unable to get pokemon"
};
case "POKEMON_LIST_SUCCESS":
return {
...state,
loading: false,
data: action.payload.results,
errorMessage: "",
count: action.payload.count
};
case "SPECIFIC_POKEMON_SUCCESS":
const typesMap = action.payload.types.map((type) => {
return type.type.name;
})
return {
...state,
data: state.data.map((pokemon) => pokemon.name === action.payload.name
? {...pokemon, types: typesMap}
: pokemon
),
loading: false,
errorMessage: ""
}
case "SPECIFIC_POKEMON_FAIL":
return {
...state,
loading: false,
errorMessage: "unable to get pokemon"
};
default:
return state
}
}
export default PokemonListReducer;
This is happening because your second useEffect does not wait for your first useEffect to finish and because of that the pokemon list is empty. On code change, since the state already has the pokemon list pre-filled, the second useEffect finds the list and does it's thing. You have to guarantee that the second action is caller right after the first one in order for this to work properly. One way to do this is to dispatch the GetSpecificPokemon action for each pokemon before finishing the GetPokemonList action. Something like this should work:
export const GetPokemonList = (page) => async (dispatch) => {
try {
dispatch({
type: "POKEMON_LIST_LOADING"
});
const perPage = 15;
const offset = (page * perPage) - perPage;
const res = await axios.get(`https://pokeapi.co/api/v2/pokemon?limit=${perPage}&offset=${offset}`);
dispatch({
type: "POKEMON_LIST_SUCCESS",
payload: res.data
});
res.data.result.forEach(pokemon => {
dispatch(GetSpecificPokemon(pokemon.name));
});
} catch (e) {
dispatch({
type: "POKEMON_LIST_FAIL"
});
}
}
Note that you won't be needing the second useEffect if you are doing this. You might also have to change displaying/not displaying the loader part yourself.
Another way is to add pokemonList as the second object in the useEffect's array parameter. I haven't tested it yet but this should work. For example:
useEffect(() => {
const fetchTypes = () => {
pokemonList.data.forEach(pokemon => {
dispatch(GetSpecificPokemon(pokemon.name));
});
}
fetchTypes();
}, [dispatch, pokemonList]);
This will call the useEffect whenever there is a change in pokemonList. In your implementation, useEffect is only called once since the value of dispatch never really changes after that. Adding pokemonList to the array results in the useEffect being called when there is a change in pokemonList also. Use this approach if you want the GetPokemonList action to always be separate from GetSpecificPokemon action i.e there are cases when both are not called together. If both are always called together then the first approach is cleaner.
That being said, these implementations actually result in a lot of network calls. The best way is to avoid the second call if possible (change your UI accordingly?) since you do not have any control over the API. If you do have control over the API you could include the extra data in the first request's response.
Edit: Here is the batch logic
const p = pokemonList.map(({ name }) =>
axios.get(`https://pokeapi.co/api/v2/pokemon/${name}`)
);
const res = await Promise.all(p);
const data = res.map((r) => ({
...r.data,
types: r.data.types.map((type) => type.type.name) // the logic you were using for types
}));
dispatch({
type: "SPECIFIC_POKEMON_SUCCESS",
payload: data
});
And then update the state in the reducer like
case "SPECIFIC_POKEMON_SUCCESS":
return {
...state,
data: action.payload,
loading: false,
errorMessage: ""
};
So I am rendering a simple post list in React.js using react-redux with axios
The data is coming from Laravel API
I have no idea what to do. I have posted my code below!
postActions.js file
export const authUserPosts = () => async (dispatch, getState) => {
try {
dispatch({
type: POST_LIST_REQUEST,
});
const {
userLogin: { userInfo },
} = getState();
const config = {
headers: {
Authorization: `Bearer ${userInfo.meta.token}`,
},
};
const { data } = await axios.get('api/v1/posts', config);
dispatch({
type: POST_LIST_SUCCESS,
payload: data,
});
} catch (error) {
dispatch({
type: POST_LIST_FAIL,
payload:
error.response && error.response.data.message
? error.response.data.message
: error.message,
});
}
};
postReducer.js
export const postAuthUserReducer = (state = { posts: [] }, action) => {
switch (action.type) {
case POST_LIST_REQUEST:
return {
loading: true,
};
case POST_LIST_SUCCESS:
return {
loading: false,
posts: action.payload,
};
case POST_LIST_FAIL:
return {
loading: false,
error: action.payload,
};
default:
return state;
}
};
Data is loaded in redux state and here's the screenshot of it
I am mapping this data inside my PostScreen.js like this.
{posts.map((element) => (
<div className="mb-4" key={element.id}>
<div>
<Link to="/" className="font-bold">
{element.body}
</Link>
<span className="text-gray-600 text-small ml-20">
{element.created_at}
</span>
<Link to="/" className="bg-red-500">
Delete
</Link>
</div>
<p className="mb-2"></p>
</div>
))}
I get the error TypeError: Cannot read property 'map' of undefined
you have data object inside posts. posts.data.map will work.