I am a bit conflicted about this and seem to find no answer.
I have a slice with some actions. What I want to do is to fire a request without awaiting it's result (it's a firebase call that should be handled optimistically).
How should I then structure my code around this case? Using createAsyncThunk shouldn't be considered because I am not awaiting anything and dont need to update any local requestStatus variables.
This is what I have right now:
// Component.js (where I do the dispatch)
useEffect(() => dispatch(updateCheckIn(payload)), [])
// CheckInActions.js
import { updateCheckInInDb } from "../../api/checkin"
import { updateCheckInAction } from "../reducers"
export const updateCheckIn = (update) => {
updateCheckInInDb(update) // make API call
return updateCheckInAction(update) // return actual reducer action
}
// CheckInReducer.js
const checkInSlice = createSlice({
name: "checkIn",
initialState: INITIAL_STATE,
reducers: {
updateCheckInAction(state, action) {
return updateStateViaUpdateApi(state.data, action)
},
},
})
export const { updateCheckInAction } = checkInSlice.actions
export default checkInSlice
This works but I feel it is a bit awkward (specially the naming). I would need to call one a updateCheckInAction and the other updateCheckIn. I am a bit new to redux toolkit. Isn't there a more elegant way of doing this?
The most idiomatic answer here would be hand-write a thunk that kicks off the async request and then dispatches the plain action:
export const updateCheckIn = (update) => {
return (dispatch) => {
// make API call
updateCheckInInDb(update)
// dispatch actual action to update state
dispatch(updateCheckInAction(update))
}
}
// used as
dispatch(updateCheckIn(123))
See https://redux.js.org/usage/writing-logic-thunks for details on what thunks are overall and how to write them.
Related
I'm new to react, redux and tyring to understand the redux-toolkit tutorial i follow. I have the slice as follows.
const initialState = {
count: 0,
};
export const counterSlice = createSlice({
name: "counter",
initialState,
reducers: {
increment: (state) => {
state.count += 1;
},
},
});
export const { increment } = counterSlice.actions;
export default counterSlice.reducer;
export const incrementTest = () => (dispatch) => {
dispatch(increment());
};
Then I use that incrementTest action as follows
<button onClick={() => dispatch(incrementTest())}> + </button>
I want to understand following.
In following fuction
export const incrementTest = () => (dispatch) => {
dispatch(increment());
};
we return a function which takes argument as dispatch then call that provided dispatch function with argement of another function increment which is defined above and exported.
However when we call this function we use dispatch(incrementTest()) providing incrementTest as a param to dispatch. I don't understand this concept . Which concept in javascript should i further study to learn this ?
Also increment reducer take state as parameter ( and action also in some cases ). Who provide this (state,action) to this function as we call it as dispatch(incrementTest())
So this:
export const incrementTest = () => (dispatch) => {
dispatch(increment());
};
is an example for a thunk, a function that gets called (by a redux middleware) with two arguments, dispatch and getState. It is typically used to coordinate async work since you can await stuff inside the function or deal with promises. Look up the thunk middleware if you want to know more. I'm not sure why they made this action a thunk, there's no need for it, maybe testing.
To your second question, the library does. All you do is call dispatch() with an action, the library calls the reducer function with the current state and your action. Think of it as emitting an event. Your job is to create the event, the library takes care of updating the global state accordingly. The reducer is written declaratively, sort of. As in "how would the global state need to change if that specific event happened?".
I am using redux in a project and I want to make a useSelector that would check to see if the values in the redux state are the default values if not it will do a request to the database and update the the state I feel like it is quite complicated though and I am having a hard time getting my head around how I need to do this.
I need to do this because sometimes the correct state is not loaded in the state I am considering just doing a check every time I use useSelector to check if the values are the default values then fetch from the database but I would much prefer to write it a way that would allow to be handled within the redux selector but I can't really grasp I how I need to do it.
const info = useSelector(getInfo)
Ideally I would like the info to be handled when I fetch here
import { SET_USER_DETAILS } from "../constants/userConstants";
const intialState = {
user: { },
};
const userReducer = (state = intialState, action: any) => {
switch (action.type) {
case SET_USER_DETAILS:
return { ...state, user: action.payload };
default:
return state;
}
};
here is what my current reducer looks like what would be the best way to do this as I am finding it a little bit difficult to follow the documentation on the redux website.
You can use redux-thunk. https://redux.js.org/usage/writing-logic-thunks
then your thunk could look something like that:
const thunkFunction = (dispatch, getState) => {
// logic here that can dispatch actions or read state
const currentState = getState() as typeof initialState;
// check if state is default state
if (JSON.stringify(initialState) === JSON.stringify(currentState)) {
fetch(url).then(data => {
dispatch({type: SET_USER_DETAILS, payload: data})
})
}
}
You need first to fetch data in react component:
const MyComponent = () => {
// while fetch is fetching, data will be default state,
// and when fetch is done, that component will automatically
// rerender with new data
const data = useSelector(getInfo);
const dispatch = useDispatch();
useEffect(() => {
dispatch(thunkFunction)
},[])
return <code>{JSON.stringify(data, null, 2)}</code>
}
I did not test it so may require some changes
but in general concept is like this
My goal is to ultimately create add a shopping cart functionality using context API. In order to do so I need to get the products from my database and store it in an array.
Currently, the challenge I'm facing is how to retrieve the data from the axios response and store it in a variable that will be passed on within a const component. Apparently, the issue is that the variable gets passed to the child before the Axios response is complete.
I tried using the await keyword, but got an error regarding not being in an async function. Hence, I tried plugging the async keyword but that didn't work as it yielded errors.
I was able to retrieve data from axios within class components with success, however, I am unable to do so in these const.
Here is my code:
Context.js
import { createContext, useContext, useReducer } from "react";
import React from "react";
import ProductService from "../services/ProductService";
import { cartReducer } from "./Reducers";
const Cart = createContext();
const Context = ({ children }) => {
let products = [];
console.log("part1", products);
ProductService.getAllProducts().then((res) => {
products = res.data; //Also tried setState({ products : res.data})
console.log("response: ", products);
});
const [state, dispatch] = useReducer(cartReducer, {
products: products,
cart: [],
});
return <Cart.Provider value={{ state, dispatch }}>{children}</Cart.Provider>;
};
export const CartState = () => {
return useContext(Cart);
};
export default Context;
ProductService.js
import axios from "axios";
const PRODUCT_BASE_URL = "http://localhost:8080/api/v1/";
class ProductService {
getAllProducts() {
return axios.get(PRODUCT_BASE_URL + "products");
}
getProductsByCategory(category) {
return axios.get(PRODUCT_BASE_URL + "products/" + category);
}
getProductById(id) {
return axios.get(PRODUCT_BASE_URL + "product/" + id);
}
}
export default new ProductService();
Reducers.js
export const cartReducer = (state, action) => {
switch (action.type) {
default:
return state;
}
};
HomeComponenet.jsx
import React from "react";
import SlideShowComponent from "./SlideShowComponent";
import HomeCategoriesComponent from "./HomeCategoriesComponent";
import FeaturedProductsComponent from "./FeaturedProductsComponent";
import { CartState } from "../context/Context";
// class HomeComponent extends React.Component
function HomeComponent() {
const { state } = CartState();
console.log("Cart Inside the Home Component: ", state);
return (
<>
<SlideShowComponent />
<div>
<HomeCategoriesComponent />
</div>
<div>
<FeaturedProductsComponent />
</div>
</>
);
}
export default HomeComponent;
First of all, I've created a fixed example here: https://stackblitz.com/edit/react-jqtcia?file=src/Context.js
You are correct, you need to await the result from axios. In React, we use the useEffect hook for things with side effects or that should not be done as part of the render. Renders in react should be non blocking, that is they should not be dependent on things like data fetching.
A simple example of this would be if we needed it in local state. This example renders without the data, then re-renders once the data is available.
const [products, setProducts] = useState([]);
useEffect(async () => {
const { data } = await ProductService.getAllProducts();
setProducts(data);
}, []);
return <div>{products.length > 0 ? `${products.length} products` : 'Loading...'</div>
NOTE: the , []); means that this will fire once, when the first render happens.
This fixes the first part of your problem, getting the result out of the request/axios.
However, the second part and most important part is that you weren't using this value. You were attempting to insert the result as part of the initial state, but this was empty by the time it was created. As you are using reducers (useReducer), this means you need to dispatch an action for each event and handle all the relevant events to data fetching in the reducer. That means, you should need to be able to handle:
Some data is in a loading state (e.g., pagination or first load)
The data failed to load
The data is partially loaded
The data has fully loaded
I've created a minimal happy example (there is no pagination and data fetching always succeeds):
Context.js
useEffect(async () => {
const { data: loadedProducts } = await ProductService.getAllProducts();
console.log('response: ', JSON.stringify(loadedProducts));
dispatch({ type: 'PRODUCTS_LOADED', products: loadedProducts });
console.log(state);
}, []);
Reducers.js
export const cartReducer = (state, action) => {
console.log(action);
switch (action.type) {
case 'PRODUCTS_LOADED':
const newState = { ...state, products: action.products };
console.log(newState);
return newState;
default:
return state;
}
};
In fact, if you'd done this with your original code, it would've worked:
ProductService.getAllProducts().then((res) => {
dispatch({ type: 'PRODUCTS_LOADED', products : res.data});
console.log("response: ", products);
});
However, this has a different bug: It will refetch the data and then dispatch the event each time Context.js is re-rendered.
Since you've asked for more information in your comment, I'll provide it here (comments were not big enough).
I've linked the relevant API documentation for the hooks above, these provide pretty good information. React does a pretty good job of explaining the what, and why of these hooks (and the library itself). Seriously, if you haven't read their documentation, you should do so.
Additional resources:
What, when and how to use useEffect - In short, if you have something to do that isn't rendering such as data fetching, it's a side-effect and should be in a useEffect.
What is a reducer in JavaScript/React/Redux - Reducers are a pattern to make shared state easier to manage and test. The basic idea is that you define a reducer, which takes an initial state and an event/action, and produces a new state. The key idea is that there must be no side-effects, the same action and state will always produce the same result, no matter the date/time, network state, etc, etc. This makes testing and reasoning about things easier, but at the cost of a more complex state management.
However, something important that I ignored in your original question is that you are kind of reinventing the wheel here. There is already a library that will centralise your state and make it available via context, it's the library that originally invented reducers: redux. I'm guessing you have read something like this article about using context instead of redux, however the advantage of redux for you is that there is a litany of documentation about how to use it and solve these problems.
My recommendation for you is to make sure you need/want redux/reducers. It has it's value and I personally love the pattern but if you are just getting started on React, you would be better off just using useState in my opinion.
I know solutions like redux thunk exist when you want to dispatch action asynchronously. However, lately I had following situation:
import {store} from "./store";
const initialState = {
todos: []
}
function todoApp(state = initialState, action) {
if(action.type == "ACTION_A"){
// 1. do smth with state
// 2. do smth with state, and then... schedule a dispatch say using setTimeout:
setTimeout(()=>store.dispatch({type:"ACTION_B", payload:1}), 2000);
return state;
}// check other actions e.g. ACTION_B etc.
return state;
}
You can see ACTION_B isn't an action I would like to dispatch from somewhere else say as an async action (so that I could use redux thunk say), rather it is part of the logic in ACTION_A.
My question is: how are such situations handled in redux?
PS. This answer, says it is fine to schedule a dispatch in reducer (my situation above) and even gives some solution using middleware. However, I followed that solution to a blog post (see comments on that answer) and saw comments by Mark Erikson (maintainer of Redux) on blog, that that is still not the right way to do it. He seems to suggest redux-loop for such situation.
My question is what are the right ways to handle such situations in redux?
Are there other solutions also apart from redux-loop?
Or can we still solve this situation using redux thunk?
This is a good case for a thunk:
const actionA = () = ({ dispatch, getState }) => {
dispatch(actionA1) // dispatch another action that will change the state
setTimeout(()=> {
const { data } = getState();
dispatch({type:"ACTION_B", payload: data });
}, 2000);
}
or a custom middleware, that will schedule the timeout, but will allow actionA to continue to the reducer, and change the state (this will happen before the timeout, because it's synchronous):
const middleware = ({ dispatch, getState }) = next => action => {
if(action.type == "ACTION_A"){ //
setTimeout(()=> {
const { data } = getState();
dispatch({type:"ACTION_B", payload: data });
}, 2000);
}
next(action);
}
In general reducers should be pure functions, ie no side effects like scheduling or dispatching actions. If an action needs to do something other than changing the state, it should use a middleware (thunk for example) to do so.
This might be a question of best practices but I'd appreciate an explanation on why this doesn't work. I'm using Typescript + Redux + Thunk and trying to call actions like this:
export const requestUserDashboards = createAction<DashboardModel>(Type.REQUEST_USER_DASHBOARDS);
Dispatch in the fetch:
export const fetchDashboards = () => {
return async (dispatch: Dispatch, getState: any) => {
try {
dispatch(requestUserDashboards({
currentDashboard: getState.currentDashboard,
dashboards: getState.dashboards,
hasDashboards: false,
error: getState.error
}))
...
}
})
}
Here's the corresponding reducer:
export const dashboardReducer = handleActions<RootState.DashboardState, DashboardModel>(
{
[DashboardActions.Type.REQUEST_USER_DASHBOARDS]: (state = initialState, action): RootState.DashboardState => ({
currentDashboard: action.payload!.currentDashboard,
dashboards: action.payload!.dashboards,
hasDashboards: action.payload!.hasDashboards,
error: action.payload!.error
})
},
initialState
);
dispatch is working, however, getState doesn't correctly collect the current store state. I'm testing this by doing the following in the component receiving the updated store:
componentWillReceiveProps(nextProps: Login.Props) {
console.log(nextProps.defaultAccounts.defaultAccount);
}
Calling this in the component using:
this.props.defaultAccountActions.fetchUserDefaultAccount();
The action is working as the values from the fetch are being captured.
However, where I am using the getState.xxxx, these values are returning as undefined:
index.tsx:84 Uncaught TypeError: Cannot read property 'defaultAccount' of undefined
The initialState from my reducer is working. I can see this from doing the console.log(this.props.defaultAccounts.defaultAccount) from the componentWillMount() function.
I'm not sure what else I can provide. I think I'm actually just fundamentally misunderstanding how actions/reducers manage the store.
Questions
I am trying to get the current store values by using the getState.xxxx in the dispatch. Is this the correct way to do this?
isn't getState a function in that place? So you would need to do something
const state = getState();
and then use state inside dispatch
found in documentation, yeah it is a function at that place so you should firstly invoke a function to get state and then use it (e.g. from documentation below)
function incrementIfOdd() {
return (dispatch, getState) => {
const { counter } = getState();
if (counter % 2 === 0) {
return;
}
dispatch(increment());
};
}
If you are using mapstatetoprops in your component you can use that to get the values from store. mapStateToProps first argument is actually the Redux state. It is practically an abstracted getState().
const mapStateToProps = function(state, ownProps) {
// state is equivalent to store.getState()
// you then get the slice of data you need from the redux store
// and pass it as props to your component
return {
someData: state.someData
}
}