I'm getting an object from an action (using axios) and using a map function to iterate it.
I also need to get another action but inside the parent object mapped.
I see that the request/response are ok (with returned data), but the reducer variable still gets empty.
1: component gets data
componentDidMount() {
const { match: { params } } = this.props;
this.props.getSaleDetails(params.order_id);
}
2: defining mapStateToProps and mapDispatchToProps
const mapStateToPropos = state => ({
saleDetails: state.salesOrders.saleDetails,
saleDetailFurthers: state.salesOrders.saleDetailFurthers
});
const mapDispatchToProps = dispatch =>
bindActionCreators({ getSaleDetails, getDetailFurthers }, dispatch);
3: creating a const from the redux props
const detailsArray = saleDetails.data;
4: iterate array with map function
// getDetailFurthers is another action, getting data by passing "detail_id" and updating "saleDetailFurthers" props
{detailsArray && detailsArray.map((item) => {
const {getDetailFurthers, saleDetailFurthers} = this.props;
getDetailFurthers(item.detail_id)
console.log(saleDetailFurthers)
// empty array????
count++;
return (
<Paper className={classes.paper} key={item.detail_id}>
// ... next code lines
5: Actions
export function getDetailFurthers(detail_id){
return async dispatch => {
const request = await axios.get(`${BASE_URL}/salesorders/detail/furthers/${detail_id}`)
return {
type: "SALE_DETAIL_FURTHERS_FETCHED",
payload: request
}
}
}
6: Reducers
const INITIAL_STATE = {
//... others
saleDetailFurthers: []
};
export default (state = INITIAL_STATE, action) => {
switch (action.type) {
///... other cases
case "SALE_DETAIL_FURTHERS_FETCHED":
return { ...state, saleDetailFurthers: action.payload }
default:
return state
}
};
I expect the "saleDetailFurthers" const be loaded with data from redux action.
You need to use dispatch instead of returning, like so:
dispatch({
type: "SALE_DETAIL_FURTHERS_FETCHED",
payload: request
});
export function getDetailFurthers(detail_id) => dispatch =>{
const request = await axios.get(`${BASE_URL}/salesorders/detail/furthers/${detail_id}`)
dispatch ({
type: "SALE_DETAIL_FURTHERS_FETCHED",
payload: request
})
}
Related
I am trying to learn Redux. what I want to do: update the store with the data I send to the reducer. what I've done so far:
let reduxData=[1,2,3];
const reduxState = useSelector((state)=>state)
const dispatch = useDispatch();
const {setReduxData} = bindActionCreators(actionCreators, dispatch);
my action:
export const setReduxData = (data) => {
return {
type: 'setReduxData',
payload: data
}
}
my reducer:
export function reducer(state = {data: []}, action) {
switch (action.type) {
case 'setReduxData':
return {
...state,
data: [action.payload]
}
default:
return state
}
}
I am still new to both programming in general and redux so I apologize for any amateur mistakes I surely have made
let reduxData=[1,2,3];
const reduxState = useSelector((state)=>state)
const dispatch = useDispatch();
dispatch(setReduxData(reduxData)) // use it as you want e.g., on button click or any other action or in useEffect
Update your action code with the following code:
export const setReduxData = (data) =>(dispatch) {
dispatch({
type: 'setReduxData',
payload: data
});
}
So I simplified my code but basically I have a straight-forward redux store:
/* store.js */
import { createStore } from 'redux';
const reducer = (state = {}, action) => {
if (action.type === 'action') state.data = data;
return state;
}
const store = createStore(reducer);
store.subscribe(() => {
console.log(store.getState()); // returns the right state, updates properly
});
export default store;
A Loader that that pulls the data from the server and dispatches it to the store:
/* Loader.js */
class Loader {
dispatch (allDocuments) {
store.dispatch({
type: 'action',
data: data
});
}
async fetchData () {
try {
const allDocuments = await ajaxCall('GET', '/fetchData');
this.dispatch(allDocuments);
return allDocuments;
} catch (e) {
console.error(e);
}
}
}
export default Loader;
And then this is my App.js file where I fire the Loader fetch method every 5 seconds and map the store state to a React component:
/* App.js */
import Loader from './Loader';
const loader = new Loader();
setInterval(async () => {
await loader.fetchData();
}, 5000);
const App = ({
data
}) => {
console.log(data); //doesn't update
return (
<div>
<p>{data}</p>
</div>
)
};
const mapStateToProps = state => ({data: state.data,})
export default connect(mapStateToProps)(App);
So the problem here is that the component does not update. Loader dispatches properly, and the redux store does get updated but the data prop in App remains an empty object, and doesn't refire the render method.
Why is mapStateToProps not updating the component when the store state changes?
mapStateToProps expects that you will not mutate the state. The problem is your reducer, which is mutating the state variable by assigning directly to state.data.
To avoid mutating the state, you'll want to return a new copy of the object whenever you change the data. Like this:
const reducer = (state = {}, action) => {
if (action.type === 'action') {
return {
...state,
data: action.payload
}
return state;
}
Of course if you only have one type of action than redux is not the right tool for the job.
Your reducer doesn't save the action.data payload. It also isn't returning a new state object reference.
const reducer = (state = {}, action) => {
if (action.type === 'action') state.data = data; // <-- mutation
return state;
}
When the action type matches then you should return a new state object reference with the action.data payload.
const reducer = (state = {}, action) => {
if (action.type === 'action') {
return {
...state,
data: action.data;
};
}
return state;
}
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.
I'm using the Redux Thunk example template. When I dispatch an action in getInitialProps, that populates my store, the data is loaded but after the page is rendered, the store is still empty.
static async getInitialProps({ reduxStore }) {
await reduxStore.dispatch(fetchCategories())
const categories = reduxStore.getState().programm.categories;
console.log('STATE!!!', categories)
return { categories }
}
The categories will load correctly but when I inspect my store, the categories state is empty.
Here is my store:
import db from '../../api/db'
// TYPES
export const actionTypes = {
FETCH_PROGRAMMS: 'FETCH_PROGRAMMS',
FETCH_CATEGORIES: 'FETCH_CATEGORIES'
}
// ACTIONS
export const fetchCategories = () => async dispatch => {
const categories = await db.fetchCategories();
console.log('loaded Cate', categories)
return dispatch({
type: actionTypes.FETCH_CATEGORIES,
payload: categories
})
}
// REDUCERS
const initialState = {
programms: [],
categories: []
}
export const programmReducers = (state = initialState, action) => {
switch (action.type) {
case actionTypes.FETCH_PROGRAMMS:
return Object.assign({}, state, {
programms: action.payload
})
case actionTypes.FETCH_CATEGORIES:
console.log('Payload!', action);
return Object.assign({}, state, {
categories: action.payload
})
default:
return state
}
}
How can I make the redux state loaded on the server (in getInitialProps) be carried over to the client?
After hours of searching for solution it seems like I found my problem. It seems like I need to pass an initialState when creating the store. So instead of this:
export function initializeStore() {
return createStore(
rootReducers,
composeWithDevTools(applyMiddleware(...middleware))
)
}
I'm doing this and it works now
const exampleInitialState = {}
export function initializeStore(initialState = exampleInitialState) {
return createStore(
rootReducers,
initialState,
composeWithDevTools(applyMiddleware(...middleware))
)
}
If you do this:
return { categories }
in getInitialProps, categories should be available in component's props in client side.
It should be available in Redux as well, this could cause the problem:
return Object.assign({}, state, {
categories: action.payload
})
Take a look at this Object.assign, the function only takes 2 parameters.
My normal way of doing this:
return {
...state,
categories: action.payload,
};
The below is one of my reducers. In the function applySetRandomImages, I want to access previous state of randomImages to add a new state of randomImages with the previous state.
How can I do that? does the reducer function in Redux provides some call back function for that? or should I implement that on my own?
// ACTIONS
const SET_RANDOM_IMAGES = "SET_RANDOM_IMAGES";
// ACTION CREATORS
function setRandomImages(randomImages) {
return {
type: SET_RANDOM_IMAGES,
randomImages
};
}
// API ACTIONS
function getRandomImages(page) {
return (dispatch, getState) => {
fetch(`/boutiques/random-images/?page=${page}`)
.then(response => response.json())
.then(json => dispatch(setRandomImages(json.results)))
.catch(err => console.log(err));
};
}
// INITIAL STATE
const initialState = {};
// REDUCER
function reducer(state = initialState, action) {
switch (action.type) {
case SET_RANDOM_IMAGES:
return applySetRandomImages(state, action);
default:
return state;
}
}
// REDUCER FUNCTIONS
function applySetRandomImages(state, action) {
const { randomImages } = action;
return {
...state,
randomImages <--- I need to merge the randomImages with a new state of randomImages
};
}
// EXPORTS
const actionCreators = {
getRandomImages,
};
export { actionCreators };
// DEFAULT REDUCER EXPORTS
export default reducer;
You can merge randomImages by spreading the old state and the new one into a new array:
function applySetRandomImages(state, action) {
const { randomImages } = action;
return {
...state,
randomImages: [...state.randomImages, ...randomImages],
};
}
Separate actions, reducers and types into their own folders.
types/index.js
export const SET_RANDOM_IMAGES = "SET_RANDOM_IMAGES";
actions/imageActions.js
import * as types from '../types';
export const getRandomImages = page => dispatch => (
fetch(`/boutiques/random-images/?page=${page}`)
.then(response => response.json())
.then(json => dispatch({ type: types.SET_RANDOM_IMAGES, payload: json.results })))
.catch(err => console.log(err))
)
From within a component, you will connect to redux state (state.images or state.images.collection) and dispatch the action (getRandomImages):
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { getRandomImages } from '../actions/imageActions';
class Example extends Component {
componentDidMount = () => this.props.getRandomImages(); // dispatches action creator
render = () => (
<div>
{ /*
The props below are redux state passed into
the component via the connect function
*/ }
{this.props.images.map(image => (
<img src={image.src} alt="image.name" />
))}
{this.props.collection.map(image => (
<img src={image.src} alt="image.name" />
))}
</div>
)
}
export default connect(state => ({
images: state.images, // pulled from redux state (state.images => this.props.images)
collection: state.images.collection // pulled from redux state (state.images.collection => this.props.images.collection)
}), { getRandomImages})(Example)
It will then trigger the AJAX request, then return a type and payload to your reducer:
reducers/index.js
import * as types from '../types'
// overwrite images for each successful fetch request
const imageReducer(state={}, {type, payload}) {
switch (type) {
// spread out any previous state, then spread out the payload (json.results)
case types.SET_RANDOM_IMAGES: return { ...state, ...payload }
default: return state;
}
}
// or append images on each successful fetch request...
const imageReducer(state={}, {type, payload}) {
switch (type) {
case types.SET_RANDOM_IMAGES:
return {
...state, // spread out any previous state
collection: [
...state.collection, // then spread out any previous "collection" state,
...payload // then spread/append the payload (json.results)
]
}
default: return state;
}
}
export default combineReducers({
images: imageReducer
});
The reducer will then spread out any previous imageReducer state, then append the res.results to it via payload. Now it exists in redux as state.images or state.images.collection. This is then pulled from redux state and into the component above as this.props.images or this.props.images.collection.
I want to access previous state of randomImages to add a new state of randomImages with the previous state
return {
...state,
...randomImages
};
If previous state was:
{
a: 1,
b: 2,
c: 3
}
And randomImages is:
{
d: 4,
e: 5
}
Then the returned new state will be
{
a: 1,
b: 2,
c: 3,
d: 4,
e: 5
}