im building a reddit clone, fetching top post. The app should have a favorites post CRUD.
So what i build is a two columns layout, left side is the post list and right part has post detail OR favorites list when switch is ON.
All this is handled by two reducers: favorites and post. The issue is: when i fire set favorites action, this is overriding post data for some reason. I just reviewed the whole flow several times and i cant fine what is going on.
This is the redux dev tools state, with post fetched and then with show favorites action
This is favorites reducer:
import {
FETCH_FAVORITES_REQUEST,
FETCH_FAVORITES_SUCCESS,
FETCH_FAVORITES_ERROR,
SHOW_FAVORITES
} from '../constants';
const initialState = {
data: [],
isLoading: false,
error: false,
show: false
};
const favorites = (state = initialState, action) => {
switch (action.type) {
case FETCH_FAVORITES_REQUEST:
return {
...state,
isLoading: true
};
case FETCH_FAVORITES_SUCCESS: {
return {
...state,
isLoading: false,
data: action.payload
};
}
case FETCH_FAVORITES_ERROR:
return { ...state, error: true, isLoading: false };
case SHOW_FAVORITES:
return { ...state, show: !state.show };
default:
return initialState;
}
};
export default favorites;
and this is posts reducer:
import {
FETCH_POST_REQUEST,
FETCH_POST_SUCCESS,
FETCH_POST_ERROR,
SET_READED,
DISMISS_ALL,
DISMISS_POST
} from '../constants';
const initialState = {
data: [],
isLoading: false,
error: false,
lastFetched: null,
allDismissed: false
};
const posts = (state = initialState, action) => {
switch (action.type) {
case FETCH_POST_REQUEST:
return {
...state,
isLoading: true
};
case FETCH_POST_SUCCESS: {
const data = [...state.data, ...action.payload];
return {
...state,
data,
lastFetched: action.lastFetched,
isLoading: false,
allDismissed: false
};
}
case FETCH_POST_ERROR:
return { ...state, error: true, isLoading: false };
case SET_READED: {
const newData = state.data;
const foundIndex = newData.findIndex((x) => x.id === action.payload);
newData[foundIndex].readed = true;
return {
...state,
data: newData
};
}
case DISMISS_ALL: {
return {
...state,
allDismissed: true,
data: []
};
}
case DISMISS_POST: {
return {
...state,
data: state.data.filter((post) => post.id !== action.payload)
};
}
default:
return initialState;
}
};
export default posts;
This is the component from where show favorites action is being dispatched:
import React from 'react';
import styled, { css } from 'styled-components';
import Toggle from 'react-toggle';
import { useDispatch, useSelector } from 'react-redux';
import { useMediaQuery } from 'react-responsive';
import { DEVICE_SIZE } from '../constants';
import { showFavorites } from '../actions/favorites';
const ToogleFavorites = styled.div`
position: fixed;
bottom: 5%;
right: 5%;
text-align: end;
z-index: 2;
${({ isMobile }) =>
isMobile &&
css`
font-size: small;
`}
`;
export default () => {
const dispatch = useDispatch();
const { show } = useSelector((state) => state.favorites);
const isTabletOrBigger = useMediaQuery({ minDeviceWidth: DEVICE_SIZE.tablet });
const handleToogle = () => dispatch(showFavorites());
return (
<ToogleFavorites isMobile={isTabletOrBigger}>
<Toggle defaultChecked={show} onChange={handleToogle} />
<div>Toogle favorites</div>
</ToogleFavorites>
);
};
favorites action (seems not big deal):
export const showFavorites = () => ({
type: SHOW_FAVORITES
});
Let me know anything extra info. Thanks a lot!
your reducer has the default case to return intialState which means any action that isn't explicitly handled resets the state. you want to return state instead.
Related
I'm trying to render the data from the following object of data which is coming from an API.
{
"code": 0,
"c": "verified",
"d": "verified",
"leaseInfo": {
"infoId": 6
},
"cpfPrice": "500.00",
"carCurrentLocation": {
"id": 1,
"carId": "df47a56a395a49b1a5d06a58cc42ffc4"
},
"n": "verified",
"p": "false",
"ownerCarInfo": {
"brand": "Ferrari",
"model": "0"
},
"serviceFeeRate": 0.10,
"depositPrice": "100.00",
"pics": [
{
"picid": 49,
"carId": "df47a56a395a49b1a5d06a58cc42ffc4"
},
],
"items": {
"itemid": 5,
"carId": "df47a56a395a49b1a5d06a58cc42ffc4"
}
}
I'm using react-redux to dispatch an action, where I will be provided with the data under a state named 'carDetails'.
However, when I try to access the data, if my component is refreshed, carDetails becomes undefined and hence gives "Cannot read property ownerCarInfo of undefined."
I'm obtaining and de-structuring the data of carDetails like this in my React component:
import React, {useEffect} from 'react';
import { useDispatch, useSelector } from 'react-redux';
const CarInfo = ({ match }) => {
const dispatch = useDispatch();
const details = useSelector((state) => state.carDetails);
const { loading, carDetails } = details;
const {pics, carCurrentLocation, items, ownerCarInfo} = carDetails;
useEffect(() => {
dispatch(getCarDetails(match.params.id));
}, [dispatch, match]);
return (
<div>
{loading ? (
<Loader></Loader>
) : (
<>
<p>{d.depositPrice}</p>
<p>{ownerCarInfo.brand}</p>
</>
)}
</div>
);
)
}
As long as the component or the React application is not refreshed, it retrieves data and displays it correctly. The carDetails becomes an empty array as soon as the page is refreshed.
This is the getCarDetails() action:
export const getCarDetails = (id) => async (dispatch, getState) => {
try {
dispatch({
type: CAR_DETAILS_REQUEST,
});
const { userLogin } = getState();
const { userInfo } = userLogin;
const config = {
headers: {
Authorization: userInfo.token,
'Content-Type': 'application/json',
},
};
const { data } = await axios.get(
`${BASE_API}/car/info/getDetails/${id}/${userInfo.bscId}`,
config
);
dispatch({
type: CAR_DETAILS_SUCCESS,
payload: data,
});
} catch (error) {
dispatch({
type: CAR_DETAILS_FAIL,
payload:
error.response && error.response.data.msg
? error.response.data.msg
: error.msg,
});
}
};
This is my reducer:
export const carsDetailsReducer = (state = { carDetails: [] }, action) => {
switch (action.type) {
case CAR_DETAILS_REQUEST:
return { loading: true };
case CAR_DETAILS_SUCCESS:
return { loading: false, carDetails: action.payload };
case CAR_DETAILS_FAIL:
return { loading: false, error: action.payload };
default:
return state;
}
};
This is how I declare carDetails in the redux store.
const reducer = combineReducers({
carDetails: carsDetailsReducer,
});
What is the cause for carDetails becoming undefined and the useEffect not running on page refresh?
If you are using axios your action should look like this with async function and await while you are calling API.
If you are passing API car id in the api link then pass the id in the parameters:
import axios from "axios";
export const loadData = (id) => async (dispatch) => {
dispatch({
type: "CAR_DETAILS_REQUEST",
});
const detailData = await axios.get("http:\\****/id");
dispatch({
type: "CAR_DETAILS_SUCCESS",
payload: {
success: detailData.data,
},
});
};
Reducer:
const initailState = { carDetails: [], loading: true };
export const carsDetailsReducer = (state = initailState, action) => {
switch (action.type) {
case CAR_DETAILS_REQUEST:
return { ...state,
loading: true
};
case CAR_DETAILS_SUCCESS:
return {...state,
loading: false,
carDetails: action.payload
};
case CAR_DETAILS_FAIL:
return { ...state,
loading: false,
error: action.payload };
default:
return ...state;
}
};
Your useEffect should only work when data is fetched:
import React, {useEffect} from 'react';
import { useDispatch, useSelector } from 'react-redux';
const CarInfo = ({ match }) => {
const dispatch = useDispatch();
const details = useSelector((state) => state.carDetails);
const { loading, carDetails } = details;
const {pics, carCurrentLocation, items, ownerCarInfo} = carDetails;
useEffect(() => {
dispatch(getCarDetails(id));
}, [dispatch]);
return (
<div>
{loading ? (
<Loader></Loader>
) : (
<>
<p>{d.depositPrice}</p>
<p>{ownerCarInfo.brand}</p>
</>
)}
</div>
You can also use it without a useEffect by making an onclick() function like this:
const loadDetailHandler = () => {
dispatch(getCarDetails(id));
};
return (
<div onClick={loadDetailHandler} >
</div>
If carDetails initial state is an array, then why are you destructuring object properties from it in your UI? Question for another time...
If after reloading the page the state reverts back to the initial state, an empty array is still a defined object. You need to track down what is causing your state.carDetails.carDetails to become undefined. If you examine your reducer notice that your CAR_DETAILS_REQUEST case wipes the carDetails state out and it becomes undefined. Honestly I'm surprised you aren't seeing this issue when your code runs normally without a page reload.
You need to hold on to that state. For good measure, you should always shallow copy the existing state when computing the next state object unless you've good reason to omit parts of state.
export const carsDetailsReducer = (state = { carDetails: [] }, action) => {
switch (action.type) {
case CAR_DETAILS_REQUEST:
return {
...state, // <-- shallow copy existing state
loading: true,
};
case CAR_DETAILS_SUCCESS:
return {
...state, // <-- shallow copy existing state
loading: false,
carDetails: action.payload
};
case CAR_DETAILS_FAIL:
return {
...state, // <-- shallow copy existing state
loading: false,
error: action.payload,
};
default:
return state;
}
};
for me, I think you should save the state in the
`case CAR_DETAILS_REQUEST:
return {
...state, // <-- shallow copy existing state
loading: true,
};
`
to be able to use it before o when you want to use a reducer you should each case
have the old state the reducer return the same sharp of initial state that put it you also used is loading and that not found in the initial state
so try to make the shape of the state
state={
isloading:false,
carDetails: []
}
also try each time to same the state by {...state ,is loading:true}
The problem is in CAR_DETAILS_REQUEST. You only return { loading: true }; so carDetails will be lost and become undefined.
Just update your reducer like this:
case CAR_DETAILS_REQUEST:
return { ...state, loading: true };
I'm trying to create a function add product into cart with redux-react
and how can I get my product info from mongoDB into initialState?
this is how my product info looks like:
img_url1: "https://thebeuter.com/wp-content/uploads/2020/06/38-1.jpg"
price: 1290000
title: "BEUTER BACK2BACK ZIPPER WHITE JACKET"
here is my reducer:
import {
ADD_PRODUCT_BASKET,
GET_NUMBERS_BASKET
} from '../actions/type'
const initialState = {
basketNumbers: 0,
cartCost: 0,
products: {
}
}
export default (state = initialState, action) => {
switch (action.type) {
case ADD_PRODUCT_BASKET:
let addQuantity = {
...state.products[action.payload]
}
console.log(addQuantity)
return {
...state,
basketNumbers: state.basketNumbers + 1,
};
case GET_NUMBERS_BASKET:
return {
...state
};
default:
return state;
}
}
Here is my github if you want to look at my code:
https://github.com/nathannewyen/the-beuter
You solve your problem using redux-saga (or redux-thunk) by fetching your data from DB before rendering your page:
productBasket.js (with redux-saga)
import axios from 'axios';
import { action as createAction } from 'typesafe-actions';
import {
put, select, takeLatest,
} from 'redux-saga/effects';
export const FETCH_PRODUCT_BASKET = 'FETCH_PRODUCT_BASKET';
export const FETCH_PRODUCT_BASKET_SUCCESS = 'FETCH_PRODUCT_BASKET_SUCCESS';
export const FETCH_PRODUCT_BASKET_ERROR = 'FETCH_PRODUCT_BASKET_ERROR';
export const actionCreators = {
fetchProductBasket: () =>
createAction(FETCH_PRODUCT_BASKET),
fetchProductBasketSuccess: (products) =>
createAction(FETCH_PRODUCT_BASKET_SUCCESS, { products }),
fetchProductBasketError: (error) =>
createAction(FETCH_PRODUCT_BASKET_ERROR, { error }),
};
export const {
fetchProductBasket,
fetchProductBasketSuccess,
fetchProductBasketError
} = actionCreators;
export const initialState = {
isFetching: false,
isError: false,
basketNumber: 0,
products: []
};
const reducer = (state = initialState, action) => {
switch (action.type) {
case FETCH_PRODUCT_BASKET:
return {
...state,
isFetching: true,
isError: false
}
case FETCH_PRODUCT_BASKET_SUCCESS:
return {
...state,
isFetching: false,
isError: false,
products: action.payload.products
}
case FETCH_PRODUCT_BASKET_ERROR:
return {
...state,
isFetching: false,
isError: true
}
default:
return state;
}
}
export default reducer;
export function* basketSaga() {
yield takeLatest(FETCH_PRODUCT_BASKET, fetchProductBasketSaga);
}
function* fetchProductBasketSaga() {
try {
// here is code with fetching data
const { data } = yield axios.get('some address');
yield put(fetchProductBasketSuccess(data));
} catch (err) {
console.log(err);
yield put(fetchProductBasketError(err));
}
}
And after that dispatch fetchProductBasket action in useEffect scope in your component. You can show skeleton to user while your data is fetching.
How do I get the state of this? I only need to put it as false and true the time I want at my components, but i`m doing something wrong, i know how do it when calling an API, but not like this.
I have this actions:
import { HIDE_MENU, ESTADO_MENU } from "./types";
export const hideMenu = dispatch => {
return dispatch({
type: HIDE_MENU
});
};
export const estadoDoMenu = open => dispatch => {
dispatch({
type: ESTADO_MENU
});
};
and this reducer:
import { HIDE_MENU, ESTADO_MENU } from "../actions/types";
const initialState = {
open: true
};
export default function(state = initialState, action) {
switch (action.type) {
case HIDE_MENU:
return {
...state,
open: false
};
case ESTADO_MENU:
console.log("chega aqui");
return {
...state
};
default:
return state;
}
}
but calling it like this:
componentDidMount() {
console.log("Estado do Menu: ", this.props.estadoDoMenu());
}
I get undefined at the console, what is wrong?
I got a problem when do objectAssign to change the state in store into a new data from server, It always get a null as the result.
i call my action in onEnter function(React-Router)
export function GET_SetupTabTitles() {
store.dispatch(getSetupTabTitles());
}
this is my action :
import {
TOGGLE_DRAWER_IN_APPBAR,
GET_SETUP_TAB_TITLES,
} from '../constants/actionTypes';
import axios from 'axios';
const ROOT_URL = 'http://localhost:8000';
export function toggleDrawerInAppBar(open){
return { type: TOGGLE_DRAWER_IN_APPBAR, openStatus: open }
}
export function getSetupTabTitles(){
return function(dispatch){
axios.get(`${ROOT_URL}/api/component/getSetupTabTitles`)
.then(response => {
dispatch({type: GET_SETUP_TAB_TITLES,
payload: response
});
});
}
}
this is my initial state on reducer :
export default {
auth: {
authenticated: (localStorage.getItem('laravel_user_token') !== null),
userinfo: {
name: null
},
error:""
},
comp: {
openDrawerStatus: false,
setupTabTitles: null,
}
};
and this is my reducer :
import {
TOGGLE_DRAWER_IN_APPBAR,
GET_SETUP_TAB_TITLES,
} from '../constants/actionTypes';
import initialState from './initialState';
import objectAssign from 'object-assign';
const compReducer = (state = initialState.comp, action) => {
switch (action.type) {
case TOGGLE_DRAWER_IN_APPBAR:
return objectAssign({}, state, {openDrawerStatus: action.openStatus});
case GET_SETUP_TAB_TITLES:
console.log(action.payload.data);
return objectAssign({}, state, {setupTabTitles: action.payload.data});
default:
return state;
}
};
export default compReducer;
when i do console.log inside
case GET_SETUP_TAB_TITLES:
it show :
Array[2]0: 0:Object 1:Object
On using JSON.stringify() it shows me [{"tabTitle":"Events"},{"tabTitle":"Tasks"}]
but my state (setupTabTitles) didn't change at all.
i do try this one :
case GET_SETUP_TAB_TITLES:
state.setupTabTitles.push(action.payload.data[0]);
return state;
it work, but i don't want to direct change the state.
You don't need to import ojectAssign from 'object-assign'; when you make use of the current ES6 syntax in your code. You only need Object.assign. Also since your action.data.payload is an array and you need to append to an array you can use the spread operator like
return {
...state,
setupTabTitles: [...state.setupTabTitles, action.payload.data]
}
Also you need to initialise you componentState to be an empty array and not null or undefined. Change that to below code
export default {
auth: {
authenticated: (localStorage.getItem('laravel_user_token') !== null),
userinfo: {
name: null
},
error:""
},
comp: {
openDrawerStatus: false,
setupTabTitles: [],
}
};
Try it like below
const compReducer = (state = initialState.comp, action) => {
switch (action.type) {
case TOGGLE_DRAWER_IN_APPBAR:
return Object.assign({}, state, {openDrawerStatus: action.openStatus});
case GET_SETUP_TAB_TITLES:
console.log(action.payload.data);
return {
...state,
setupTabTitles: [...state.setupTabTitles, ...action.payload.data]
}
default:
return state;
}
};
The syntax of objectAssign is different from what I use, you can see it here
var state = {
openDrawerStatus: false,
setupTabTitles: [],
}
var payload = [{"tabTitle":"Events"},{"tabTitle":"Tasks"}]
console.log( {
...state,
setupTabTitles: [...state.setupTabTitles, ...payload]
});
As you are already using ES6, you could just use the object spread operator and get rid of the object-assign library, it would be like this:
import {
TOGGLE_DRAWER_IN_APPBAR,
GET_SETUP_TAB_TITLES,
} from '../constants/actionTypes';
import initialState from './initialState';
const compReducer = (state = initialState.comp, action) => {
switch (action.type) {
case TOGGLE_DRAWER_IN_APPBAR:
return { ...state, openDrawerStatus: action.openStatus };
case GET_SETUP_TAB_TITLES:
return { ...state, setupTabTitles: action.payload.data };
default:
return state;
}
};
export default compReducer;
In your initial state, I would change setupTabTitle from null to an empty array []:
setupTabTitles: [],
And in your reducer, append data to this array:
const compReducer = (state = initialState.comp, action) => {
switch (action.type) {
...
case GET_SETUP_TAB_TITLES:
return {
...state,
setupTabTitles: [
...state.setupTabTitles,
...action.payload.data
]
}
...
}
};
Or if you don't want to append, just replace, I would do:
setupTabTitles: [
...action.payload.data
]
I'm using this package https://github.com/RealScout/redux-infinite-scroll to make infinite scroll on list of brand. Here is my code:
Container:
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { actions, getBrands } from '../reducer';
import Infinite from 'react-infinite';
import InfiniteScroll from 'redux-infinite-scroll';
import SearchBox from '../components/SearchBox';
import CardList from '../components/CardList';
const { fetchBrands } = actions;
class BrandList extends Component {
componentDidMount() {
this.props.fetchBrands({ page: 1 });
}
renderList() {
const brands = this.props.brands;
return brands.map((brand) => {
return (
<CardList key={brand.id} name={brand.name} avatar={brand.avatar.thumbnail} follower={brand.follows_count} />
);
});
}
toggle() {
return this.props.isFetching;
}
loadMore() {
const {lastPage, currentPage} = this.props;
const nextPage = currentPage ? parseInt(currentPage) + 1 : 1;
if(currentPage && currentPage <= lastPage){
this.props.fetchBrands({page: nextPage});
}
}
render() {
return (
<div>
<SearchBox />
<div className="row">
<InfiniteScroll
items={this.renderList()}
loadMore={this.loadMore.bind(this)}
/>
</div>
</div>
);
}
}
function mapStateToProps(state) {
return {
brands: getBrands(state),
isFetching: state.brand.isFetching,
currentPage: state.brand.currentPage,
lastPage: state.brand.lastPage
};
}
export default connect(mapStateToProps, { fetchBrands })(BrandList);
Reducer:
import axios from 'axios';
// Define Types
export const types = {
// brand list
FETCH_BRANDS: 'fetch_brands',
FETCH_BRANDS_SUCCESS: 'fetch_brands_success',
FETCH_BRANDS_ERROR: 'fetch_brands_failure',
FETCH_BRAND: 'fetch_brand',
FETCH_BRAND_SUCCESS: 'fetch_brand_success',
FETCH_BRAND_ERROR: 'fetch_brand_failure',
};
const { FETCH_BRANDS, FETCH_BRANDS_SUCCESS, FETCH_BRANDS_ERROR } = types;
// Define Reducer
export const INITIAL_STATE = { brands: [], brand: {}, isFetching: false, error: null, currentPage: 1 };
export default function (state = INITIAL_STATE, action) {
switch (action.type) {
case FETCH_BRANDS:
return { ...state, isFetching: true };
case FETCH_BRANDS_SUCCESS:
return { ...state, brands: action.payload.brands.data, currentPage: action.payload.brands.current_page, lastPage: action.payload.brands.last_page };
case FETCH_BRANDS_ERROR:
return { ...state, error: action.payload };
default:
return state;
}
}
// Define Actions
export const actions = {
fetchBrands: ({page, count = 15}) => {
return (dispatch) => {
dispatch({ type: FETCH_BRANDS });
axios.get(`brands?page=${page}&count=${count}`)
.then((response) => {
const {data} = response;
if (data.code == 200) {
dispatch({ type: FETCH_BRANDS_SUCCESS, payload: data });
}
});
};
}
};
// SELECTOR
export const getBrands = (state) => state.brand.brands;
it run loadMore function successfully but it not extend current list, it replace it instead.
loadmore function only run once. it should run 10 times.
do I miss something on my code to make it scroll?
Try adding
brands: [ ...state.brands, ...action.payload.brands.data]
like this in your reducer
case FETCH_BRANDS_SUCCESS:
return { ...state, brands: [ ...state.brands, ...action.payload.brands.data], currentPage: action.payload.brands.current_page, lastPage: action.payload.brands.last_page };
Which means that you are concating current list with upcoming list (versioned data)