how to render updated state in react-redux? - reactjs

I am trying to update state and render the updated value in my component but unable to achieve it.
<--Here is my action -->
export const fetchProducts = () => async (dispatch) => {
const res = await fakeApi.get("/products");
console.log(res.data);
dispatch({
type: FETCH_PRODUCTS,
payload: res.data,
});
};
<--Here is the reducer -->
const initialState = {
products: [],
};
export default (state = initialState, action) => {
const { type, payload } = action;
switch (type) {
case FETCH_PRODUCTS:
return { ...state, products: payload };
default:
return state;
}
};
<--Here is the component -->
const Products = ({ products, fetchProducts }) => {
const [productsList,setProductsList]=useState([])
useEffect(() => {
fetchProducts();
console.log(products);
setProductsList({productsList:products})
}, []);
return <ProductList products={productsList} />;
};
const mapStateToProps = (state) => ({
products: state.products,
});
export default connect(mapStateToProps, { fetchProducts })(Products);
<-- This is the first time I am using redux. I don't know where I am going wrong.All i am getting is undefined in the state. -->

you are using react hooks for a function-based component, using useSelector() hook instead of mapStateToProps might be easier in this specific scenario to get data from redux store, as i can't see what you've named your reducer in the root reducer, i'm assuming it's named productReducer, try this in your component:
import {useSelector} from 'react-redux'
...
let products = useSelector(state=>state.productReducer.Products)

Related

Redux reducer not Updating State in Next.js server-side-rendering

Problem: Action is dispatching and it's checked by reducer action.type. but after checking the reducer not updating the state. reducer returning default state.
Action is dispatching from [id].js
const RoomDetailsPage = () => {
const router = useRouter();
const { id } = router.query;
return <div>
<RoomDetails/>
</div>;
};
export const getServerSideProps= wrapper.getServerSideProps(
(store)=>(
async ({ req, res, query })=>{
store.dispatch(getRoomDetails(query?.id))
}
)
)
export default RoomDetails;
roomAction.js
export const getRoomDetails = (id) => async (dispatch) => {
try {
const { data } = await getRoomDetailsApi(id);
dispatch({
type: ROOM_DETAILS_SUCCESS,
payload: data,
});
} catch (error) {
dispatch({
type: ROOM_DETAILS_FAIL,
payload: error?.response?.data?.message,
});
}
};
roomReducer.js
type case is matching in switch block, also payload coming with action. but reducer not updating the state.
export const roomDetailsReducer = (state = { room: null, error:null}, action) => {
switch (action.type) {
case ROOM_DETAILS_SUCCESS:
return {room:{hi:'hello'}};
case ROOM_DETAILS_FAIL:
return {error: action.payload};
case CLEAR_ERRORS:
return {error: null};
default:
return state;
}
};
combineReducers code in reducer.js
import { combineReducers } from "redux";
import { allRoomsReducers, roomDetailsReducer } from "./roomReducers";
const reducers = combineReducers({
allRooms: allRoomsReducers,
roomDetails: roomDetailsReducer,
})
export default reducers
Note: allRoomsReducers code working fine.

store.getState() returning empty value in index.js

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.

useReducer + Typescript on a async maner

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.

Redux Thunk action with axios returning multiple values

I have a React app that uses redux-thunk and axios to fetch an API. The action fires successfully, but returns multiple payloads which means it is firing multiple times.
How can I make it fire only one time?
Code
Actions
import Axios from "axios";
import { fetchEnglishLeagueTable } from "./ActionTypes";
export function fetchEnglishTable() {
var url = "https://api.football-data.org/v2/competitions/PL/matches";
var token = "52c8d88969d84ac9b17edb956eea33af";
var obj = {
headers: { "X-Auth-Token": token }
};
return dispatch => {
return Axios.get(url, obj)
.then(res => {
dispatch({
type: fetchEnglishLeagueTable,
payload: res.data
});
})
.catch(e => {
console.log("Cant fetch ", e);
});
};
}
Reducers
import { fetchEnglishLeagueTable } from "../actions/ActionTypes";
const initialState = {
EnglishTable: {}
};
const rootReducer = (state = initialState, action) => {
switch (action.type) {
case fetchEnglishLeagueTable:
return {
...state,
EnglishTable: action.payload
};
default:
return state;
}
};
export default rootReducer;
Page
const League = props => {
useEffect(() => {
props.getLeagueTable();
}, [props.leagueTable]);
console.log(props.leagueTable);
return <p>ihi</p>;
};
const mapStateToProps = state => ({
leagueTable: state.EnglishTable
});
const mapDispatchToProps = dispatch => {
return { getLeagueTable: () => dispatch(fetchEnglishTable()) };
};
export default connect(mapStateToProps, mapDispatchToProps)(League);
Store
import rootReducer from "./Reducer";
import thunk from "redux-thunk";
const store = createStore(rootReducer, applyMiddleware(thunk));
export default store;
Here is what it returns
Just remove leagueTable from useEffect's dependency array, so it'll fetch them only once component is mounted. Because now you have a loop:
Get leagues -> leagueTable updates -> useEffect sees that leagueTable changed in dependency array and calls to get leagues again and we've got a loop.
const League = props => {
useEffect(() => {
props.getLeagueTable();
}, []); // <~ no props.leagueTable here
console.log(props.leagueTable);
return <p>ihi</p>;
};
Hope it helps :)

How do I access state across multiple Contexts if I only have a single Provider in React?

I was going over the updated Stephen Girder react-native course and saw that he used a single createDataContext file so that his Reducer has access to Context and Provider.
his createDataContext file looks like this:
import React, {useReducer} from 'react';
export default (reducer, actions, initialState) => {
const Context = React.createContext(reducer, initialState);
console.log('show me the initial state: ', initialState)
// actions === {addBlogPost: (dispatch) => { return ()=> {} }}
const Provider = ({children}) => {
const [state, dispatch] = useReducer(reducer, initialState);
const boundActions = {};
for (let key in actions) {
boundActions[key] = actions[key](dispatch);
}
return (
<Context.Provider value={{state, ...boundActions}}>
{children}
</Context.Provider>
);
};
return {Context, Provider};
};
BlogPostContext
import createDataContext from './createDataContext';
const blogReducer = (state, action) => {
console.log('show me the state inside the reducer: ', action);
switch (action.type) {
case 'add_blogpost':
return [
...state,
{
id: Math.floor(Math.random() * 999),
title: action.payload.title,
content: action.payload.content,
},
];
case 'delete_blogpost':
return state.filter(blogPost => blogPost.id !== action.payload);
case 'edit_blogpost':
return state.map(blogPost => {
return blogPost.id === action.payload.id ? action.payload : blogPost;
});
default:
return state;
}
};
const addBlogPost = dispatch => {
return (title, content, callback) => {
console.log("inside addBlogpost", title, content);
dispatch({
type: "add_blogpost",
payload: { title, content }
});
callback();
};
};
const deleteBlogPost = dispatch => {
return id => {
dispatch({type: 'delete_blogpost', payload: id});
};
};
const editBlogPost = dispatch => {
return (id, title, content) => {
dispatch({
type: "edit_blogpost",
payload: { id, title, content }
});
};
};
export const {Context, Provider} = createDataContext(
blogReducer,
{addBlogPost, deleteBlogPost, editBlogPost},
[],
);
Methods that need to be accessed on a particular page are simply de-destructured on the page that they're needed:
import React, {useContext} from 'react';
import {StyleSheet} from 'react-native';
import {Context} from '../context/BlogContext';
import BlogPostForm from '../components/BlogPostForm';
const CreateScreen = ({navigation}) => {
const {addBlogPost} = useContext(Context);
return (
<BlogPostForm
onSubmit={(title, content) => {
addBlogPost(title, content, () => navigation.navigate('Homepage'));
}}
/>
);
};
const styles = StyleSheet.create({});
export default CreateScreen;
I would like to create a second Context page that contains a different reducer, methods, and most importantly initialState object.
However, I will NOT be able to access the state of the second Context page because in the createDataContext only passes state as a value to a provider.
<Context.Provider value={{state, ...boundActions}}>
That only grants me access to the first Context(BlogContext). How can I access the state of the second Context page given how createDataContext file is at the moment? Would I need to create a second createDataContext or do I need to define individual states when I pass the state in the Context.Provider?

Resources