import api from "api";
import { Loader } from "components/base";
import { useReducer } from "react";
import { useQuery } from "react-query";
function reducer(state, { type, payload }) {
switch (type) {
case "init":
return state.concat(payload);
default:
throw new Error();
}
}
function Table() {
const [workers, dispatch] = useReducer(reducer, []);
const fetchWorkers = async () => {
const workersData = await api.index();
return workersData;
};
const { status, data, error } = useQuery("", fetchWorkers);
switch (status) {
case "loading":
return <Loader />;
case "error":
return <p className="text-red-600">{error.message}</p>;
case "success":
dispatch({ type: "init", payload: data });
return <p>Success!</p>;
default:
return <p>💩</p>;
}
}
export default Table;
The above code causes infinite re-renders? Why? 😕
Once useQuery communicates that status === "success", why can't I just dispatch and initialize my data? How else should I be doing this instead? 😖
I removed useQuery and just did with a useEffect without any issue - the data came back and I dispatched.
What is different here with useQuery?
Your code is causing infinite re-renders because you're calling dispatch every render after the data has loaded:
case "success":
// since the switch() statement runs every render, as long as it's "success"
// we'll call dispatch, update the reducer, and then force a re-render
// thus causing a loop
dispatch({ type: "init", payload: data });
return <p>Success!</p>;
There are two ways you can avoid this.
Don't put the data into your reducer. It doesn't look like you have any reason to do that in your sample code, and in case the data is needed in more places, you could potentially just call useQuery there as well (I'm not terribly familiar with the library but I imagine they have some caching strategy).
If you really need the data in the reducer, do the dispatch within an effect so it only runs the moment status changes to success:
React.useEffect(() => {
if (status === 'success') {
dispatch({ type: 'init', payload: data });
},
}, [status]); // since `status` is in the dependency, this effect only runs when its value changes
General lesson is try to avoid calling state-changing functions directly within the render body of your component--prefer effects and callbacks.
Related
I'm trying to resolve this issue, and I'm almost there. I'm getting the correct data from the API, and it's updating when it should, but on initial load useSWR is hitting the API with all null data.
The data come from useContext, and are set in a useEffect hook in a parent of the component that calls useSWR.
I guess what's happening is that the since useEffect isn't called until after initial hydration, the component with useSWR is being rendered before it has data.
But if the context setter isn't wrapped in a useEffect, I get
Warning: Cannot update a component (`ContestProvider`) while rendering a different component (`PageLandingPage`). To locate the bad setState() call inside `PageLandingPage`, follow the stack trace as described in https://reactjs.org/link/setstate-in-render
and it's stuck in an infinite loop.
I could probably stop this by putting some checks in the fetcher, but that seems like a hack to me. The useSWR documentation addresses the case of fetching data server side and making it available to multiple components right in the Getting Started section, but what's the correct way to get data from the client that needs to be used in multiple components, including ones that want to fetch data from the server based on the client data?
EDIT: Since originally asking the question, I've discovered conditional fetching, and the third option there seems nearly a perfect fit, but I'm using a complex key to a custom fetcher, and the data for the key aren't coming from another useSWR call, as in the example — they're coming from the useContext which has the unfortunate difference that, unlike the example, the data are null instead of undefined, so it won't throw.
How can I use this conditionality with data coming in from the useContext?
Here's the app hierarchy:
<MyApp>
<ContestEntryPage>
<ContestProvider> // context provider
<PageLandingPage> // sets the context
<Section>
<GridColumn>
<DatoContent>
<ContestPoints> // calls useSWR with data from the context
Here's the useSWR call:
// /components/ContestPoints.js
const fetcher = async ({pageId, contestId, clientId}) => {
const res = await fetch(`/api/getpoints?pageId=${pageId}&clientId=${clientId}&contestId=${contestId}`);
if (!res.ok) {
const error = new Error('A problem occured getting contest points');
error.info = await res.json();
error.status = res.status;
throw error;
}
return res.json();
}
const ContestPoints = () => {
const { contestState } = useContest();
// XXX should be conditional on the `contestState` parameters
const { data: points, error } = useSWR({
pageId: contestState.pageId,
contestId: contestState.contestId,
clientId: contestState.clientId
}, fetcher);
if (error) {
logger.warn(error, `Problem getting contest points: ${error.status}: ${error.info}`);
}
return (
<p>{points?.points || 'Loading...'}</p>
)
}
export default ContestPoints
It seems like finding a way to make that do the conditional fetching is likely best, but in case it's more elegant to leave the useSWR call as is, and address this farther up the chain, here are the other relevant pieces of code.
The context is being set based on information in localStorage:
// /components/PageLandingPage.js
import { useContest } from '../utils/context/contest';
const PageLandingPage = ({ data }) => {
const { dispatchContest } = useContest(); // wrapper around useContext which uses useReducer
useEffect(() => {
// Don't waste the time if we're not a contest page
if (!data?.contestPage?.id) return;
const storedCodes = getItem('myRefCodes', 'local'); //utility function to retrieve from local storage
const refCodes = storedCodes ? JSON.parse(storedCodes)?.refCodes : [];
const registration = refCodes
.map((code) => {
const [ contestId, clientId ] = hashids.decode(code);
return {
contestId: contestId,
clientId: clientId
}
})
.find((reg) => reg.contestId && reg.clientId)
dispatchContest({
payload: {
pageId: data.contestPage.id,
contestId: registration.contestId,
clientId: registration.clientId,
registrationUrl: landingPage?.registrationPage?.slug || ''
},
type: 'update'
})
}, [data, dispatchContest])
...
And the context wrapper is initialising with null state:
const initialState = {
contestId: null,
clientId: null
};
const ContestContext = createContext(initialState);
function ContestProvider({ children }) {
const [contestState, dispatchContest] = useReducer((contestState, action) => {
return {
...contestState,
...action.payload
}
}, initialState);
return (
<ContestContext.Provider value={{ contestState, dispatchContest }}>
{children}
</ContestContext.Provider>
);
}
function useContest() {
const context = useContext(ContestContext);
if (context === undefined) {
throw new Error('useContest was used outside of its provider');
}
return context;
}
export { ContestProvider, useContest }
I'm not sure it's the best solution, but I ended up resolving this by using the first example in the documentation, using null and a new field in the context:
const { data: points, error } = useSWR(contestState?.isSet ? {
pageId: contestState.pageId,
contestId: contestState.contestId,
clientId: contestState.clientId
} : null, fetcher);
and the contestState.isSet gets set in the context:
const initialState = {
isSet: false,
contestId: null,
clientId: null
};
and update it when all the other fields get set:
dispatchContest({
payload: {
isSet: true,
pageId: data.contestPage.id,
contestId: registration.contestId,
clientId: registration.clientId,
registrationUrl: landingPage?.registrationPage?.slug || ''
},
type: 'update'
})
I have a pretty simple use case - I have a global app context where I'm trying to store data fetched from an endpoint. My goal is to load this data into the context on app load and I'm going about it using the useReducer hook. I settled on the solution of calling an action getIssuerDetails() that dispatches various states throughout the method and invokes the issuerApi service to actually call the API (it's a simple Axios GET wrapper). This action is called from a useEffect within the Provider and is called on mount as shown below.
I'm having some trouble wrapping my head around how to properly test that 1) my AppProvider actually gets populated with the data fetched within the useEffect and 2) my child components within my AppProvider are being populated correctly with the data passed down from the provider. Am I approaching this data fetching portion correctly? I can either make the actual API call within my App component on mount and then dispatch actions to update the global state OR I keep my solution of fetching my data from within the useEffect of the provider.
I know I'm not supposed to be testing implementation details but I'm having a hard time separating out what data/methods I should mock and which ones I should allow to execute on their own. Any help would be greatly appreciated.
AppContext.tsx
import { createContext, FC, useEffect, useContext, useReducer, useRef } from 'react';
import { getIssuerDetails } from './issuer/actions';
import { appStateReducer } from './global/reducer';
import { combineReducers } from '#utils/utils';
import { GlobalAppStateType } from './global/types';
/**
* Our initial global app state. It just stores a bunch
* of defaults before the data is populated.
*/
export const defaultInitialState = {
issuerDetails: {
loading: false,
error: null,
data: {
issuerId: -1,
issuerName: '',
ipoDate: '',
ticker: '',
},
},
};
export type AppStateContextProps = {
state: GlobalAppStateType;
};
export type AppDispatchContextProps = {
dispatch: React.Dispatch<any>;
};
export const AppStateContext = createContext<AppStateContextProps>({
state: defaultInitialState,
});
export const AppDispatchContext = createContext<AppDispatchContextProps>({
dispatch: () => null,
});
/**
*
* #param
* #returns
*/
export const mainReducer = combineReducers({
appState: appStateReducer,
});
export type AppProviderProps = {
mockInitialState?: GlobalAppStateType;
mockDispatch?: React.Dispatch<any>;
};
/**
* Our main application provider that wraps our whole app
* #param mockInitialState - mainly used when testing if we want to alter the data stored in our
* context initially
* #param children - The child components that will gain access to the app state and dispatch values
*/
export const AppProvider: FC<AppProviderProps> = ({ mockInitialState, mockDispatch, children }) => {
const [state, dispatch] = useReducer(mainReducer, mockInitialState ? mockInitialState : defaultInitialState);
const nState = mockInitialState ? mockInitialState : state;
const nDispatch = mockDispatch ? mockDispatch : dispatch;
// Ref that acts as a flag to aid in cleaning up our async data calls
const isCanceled = useRef(false);
useEffect(() => {
async function fetchData() {
// Await the API request to get issuer details
if (!isCanceled.current) {
await getIssuerDetails(nDispatch);
}
}
fetchData();
return () => {
isCanceled.current = true;
};
}, [nDispatch]);
return (
<AppStateContext.Provider value={{ state: nState }}>
<AppDispatchContext.Provider value={{ dispatch: nDispatch }}>{children}</AppDispatchContext.Provider>
</AppStateContext.Provider>
);
};
/**
* Custom hook that gives us access to the global
* app state
*/
export const useAppState = () => {
const appStateContext = useContext(AppStateContext);
if (appStateContext === undefined) {
throw new Error('useAppState must be used within a AppProvider');
}
return appStateContext;
};
/**
* Custom hook that gives us access to the global
* app dispatch method to be able to update our global state
*/
export const useAppDispatch = () => {
const appDispatchContext = useContext(AppDispatchContext);
if (appDispatchContext === undefined) {
throw new Error('useAppDispatch must be used within a AppProvider');
}
return appDispatchContext;
};
AppReducer.ts
Note: Code still needs to be cleaned up here but it's functioning at the moment.
import * as T from '#context/global/types';
export const appStateReducer = (state: T.GlobalAppStateType, action: T.GLOBAL_ACTION_TYPES) => {
let stateCopy;
switch (action.type) {
case T.REQUEST_ISSUER_DETAILS:
stateCopy = { ...state };
stateCopy.issuerDetails.loading = true;
return stateCopy;
case T.GET_ISSUER_DETAILS_SUCCESS:
stateCopy = { ...state };
stateCopy.issuerDetails.loading = false;
stateCopy.issuerDetails.data = action.payload;
return stateCopy;
case T.GET_ISSUER_DETAILS_FAILURE:
stateCopy = { ...state };
stateCopy.issuerDetails.loading = false;
stateCopy.issuerDetails.error = action.payload;
return stateCopy;
default:
return state;
}
};
getIssuerDetails()
export const getIssuerDetails = async (dispatch: React.Dispatch<any>) => {
dispatch({ type: GlobalState.REQUEST_ISSUER_DETAILS, payload: null });
try {
// Fetch the issuer details
const response = await issuerApi.getIssuerDetails(TEST_ISSUER_ID);
if (response) {
/***************************************************************
* React Testing Library gives me an error on the line below:
* An update to AppProvider inside a test was not wrapped in act(...)
***************************************************************/
dispatch({ type: GlobalState.GET_ISSUER_DETAILS_SUCCESS, payload: response });
return response;
}
// No response
dispatch({
type: GlobalState.GET_ISSUER_DETAILS_FAILURE,
error: { message: 'Could not fetch issuer details.' },
});
} catch (error) {
dispatch({ type: GlobalState.GET_ISSUER_DETAILS_FAILURE, error });
}
};
dashboard.test.tsx
import { render, screen, cleanup, act } from '#testing-library/react';
import { AppProvider, AppStateContext } from '#context/appContext';
import { GlobalAppStateType } from '#context/global/types';
afterEach(() => {
cleanup();
jest.clearAllMocks();
});
describe('Dashboard page', () => {
it('should render the page correctly', async () => {
act(() => {
render(
<AppProvider>
<Dashboard />
</AppProvider>
);
});
expect(await screen.findByRole('heading', { level: 1 })).toHaveTextContent('Stock Transfer');
});
});
I won't dive into the code specifically since there is too much you want to test all at once.
From what I could gather, you are trying to do an Integration Test and not a Unitary Test anymore. No problem there, you just need to define where you want to draw the line. For me, it's pretty clear that the line lies in the issuerApi.getIssuerDetails call, from which you could easily mock to manipulate the data how you want.
1) my AppProvider actually gets populated with the data fetched within the useEffect and 2) my child components within my AppProvider are being populated correctly with the data passed down from the provider.
Well, I would advise you to make a simple mock component that uses the hook and displays the data after fetching. You could make a simple assertion for that, no need for an actual component (<Dashboard />).
Am I approaching this data fetching portion correctly?
It all depends on how you want to structure it but ideally the AppProvider should be thin and lay those data fetching and treatments inside a service just for that. This would provide a better way to unit test the components and smoother code maintenance.
I am new in react js. I have started doing a small product with react-redux. I am using saga middle-ware.
What i have done is as under.
This is the component
//all import work
import { activateAuthLayout, onLoad } from '../../../store/actions';
class EcommerceProductEdit extends Component {
constructor(props) {
super(props);
this.state = {
checked: false,
unselected_lists: [],
main_checked: false
}
//here I get the products props always null
console.log(this.props);
}
componentDidMount() {
this.props.activateAuthLayout();
//dispatching an action to fetch data from api, done in midddleware
if (this.props.user !== null && this.props.user.shop_id)
this.props.onLoad({
payload: this.props.user
});
}
render() {
//here I get the products props
console.log(this.props);
return (
//jsx work
);
}
}
const mapStatetoProps = state => {
const { user, is_logged_in } = state.Common;
const { products, is_loading } = state.Products;
return { user, is_logged_in, products, is_loading };
}
export default withRouter(connect(mapStatetoProps, { activateAuthLayout, onLoad })(EcommerceProductEdit));
Action is
import { FETCH_PRODUCT, FETCH_PRODUCT_SUCCESS } from './actionTypes';
export const onLoad = (action) => {
return {
type: FETCH_PRODUCT,
payload: action.payload
}
}
export const productFetched = (action) => {
return {
type: FETCH_PRODUCT_SUCCESS,
payload: action.payload
}
}
Reducer is
import { FETCH_PRODUCT_SUCCESS } from './actionTypes';
const initialState = {
products: null,
is_loading: true
}
export default (state = initialState, action) => {
switch (action.type) {
case FETCH_PRODUCT_SUCCESS:
state = {
...state,
products: action.payload,
is_loading: false
}
break;
default:
state = { ...state };
break;
}
return state;
}
And saga is
import { takeEvery, put, call } from 'redux-saga/effects';
import { FETCH_PRODUCT } from './actionTypes';
import { productFetched } from './actions';
import agent from '../../agent';
function* fetchProduct(action) {
try {
let response = yield call(agent.Products.get, action.payload);
yield put(productFetched({ payload: response }));
} catch (error) {
if (error.message) {
console.log(error);
} else if (error.response.text === 'Unauthorized') {
console.log(error)
}
}
}
function* productSaga() {
yield takeEvery(FETCH_PRODUCT, fetchProduct)
}
export default productSaga;
I am being able to get the products props only in render function. How would i be able to get it it in constructor ?
I would be really grateful if anyone explained me about react life cycle a little bit more.
Thanks.
updated
a constructor is called during object instantiation. According to the docs "The constructor for a React component is called before it is mounted". So if the props passed to the component are being changed after the component has been mounted you can use componentWillReceiveProps life cycle methods.
componentWillReceiveProps is deprecated so you can use componentDidUpdate instead. Example from the docs.
componentDidUpdate(prevProps) {
// Typical usage (don't forget to compare props):
if (this.props.userID !== prevProps.userID) {
// update your component state from here.
this.fetchData(this.props.userID);
}
}
MiddleWare: Middleware just comes in between the flow after the action has been dispatched and before it reaches the reducers, like in your case once you fire onLoad action and before it reaches the reducers, its caught in Saga middleware which executes it according to code written in it
Lifecycle in your case goes the following way:
In your compoenentDidMount method, you dispatch an action of onLoad. The action type in such a case becomes "FETCH_PRODUCT" and same action is now caught in Saga.
Since this is async call, the code in your component continues executing while the Saga perform its action in parallel. It calls API through this line of code: yield call(agent.Products.get, action.payload); . Once API call is completed, it dispatches an action 'productfetched' through this line of code yield put(productFetched({ payload: response }));.
Now this action reaches reducer and modify the state of "products". Since the product state in your redux is modified, your component EcommerceProductEdit re-renders and you get your product list in render method. The point to be noted is that the flow must have already finished executing inside componentDidMount method by this time, so no chance of having products their
Solution to your problem:
Once an action is dispatched and which has become async due to Saga, you won't be able to get value in constructor, if you use Saga. You can just directly call upon the API using axios/fetch library in componentDidMount and await their (Making it synchronous). Once you get response, you may proceed further
In case you have functional component, then you may use Effect hook and bind the dependency to products state. You can write your code in this block, what you want to be executed after API call is made and product list modifies.
React.useEffect(
() => {
// You code goes here
},
[products]
);
You just have to console props rather than doing this.props. You should not reference props with this inside the constructor.
Do this instead:
console.log(props)
Middleware is not related to react lifecycle at all, other than it updates and connected components "react" to props updating.
Check the constructor docs
https://reactjs.org/docs/react-component.html#constructor
Question: why are you trying to log props in the constructor anyway? If you want to know what the props are, use one of the lifecycle functions, componentDidMount/componentDidUpdate, don't use the render function to do side-effects like make asynchronous calls or console log.
componentDidMount() {
console.log(this.props);
}
If you must log props in the constructor though, access the props object that was passed as the component won't have a this.props populated yet.
constructor(props) {
super(props);
...
console.log(props);
}
I am having a problem, I believe many of you have faced it somehow, so I am using axios to get my data :
let data = axios.get(
"http://localhost:8080/api/taskExecution?&search=&page=1&size=8"
).then(response => {
if (response.status !== 200) {
console.log("LOADING");
} else {
return response.data;
}
});
let tasks = [];
data.then(response => {
tasks = response;
});
console.log(tasks);
return tasks;
Something like this, response data returns array of 8 items, but tasks is still empty which is normal because the axios request takes some time, I can use setTimeout for 100ms and inside it, I put console.log(tasks); and it will work but is not a proper solution, because what if the server takes 5s to returns the data?
This code is in my reducer, so I have to get my tasks and return them so I can display them, and show a loader when the request is executed.
This is my reducer :
import update from "immutability-helper";
import Axios from "axios";
export default function getTasksReducer(state = [], action) {
switch (action.type) {
case "GET_TASKS":
let data = Axios.get(
"http://localhost:8080/api/taskExecution?&search=&page=1&size=8"
).then(response => {
if (response.status !== 200) {
console.log("LOADING");
} else {
return response.data;
}
});
let tasks = [];
data.then(response => {
tasks = response;
});
console.log(tasks);
return tasks;
default:
return state;
}
}
I need some help in this code and also in the conception of it, I mean where should I change my loader state and so forth.
I guess you can spent some time in understanding Promises and async/awiat
For eg: if you do this, your console will have all tasks listed.
data.then(response => {
tasks = response;
console.log(tasks);
});
Reason for that is the function you are passing to .then function of a promise is not executed immediately, its executed once a Promise is resolved(In your case after finishing execution of the http request.)
To get a more line by line execution feel you can use async/await
let tasks = await Axios.get("http://localhost:8080/api/taskExecution?&search=&page=1&size=8")
.then(response => {
if (response.status !== 200) {
console.log("LOADING");
} else {
return response.data;
}
});
console.log(tasks);
return tasks;
The catch is that you can use await only inside an async function
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function
Alright buddy, so there're a few things I want to raise up:
First, you should always use your reducers only return new state to Redux, and that's it. You just merge new data that comes from actions, with your old state and return it back. And you can't use Promise or async/await in there because Redux doesn't and won't support that behavior.
Second, all the business logic should be placed in your actions. Data fetching(like in your case), computations and that kind of stuff needs be in actions. And now, you've come to the point where you, most likely, should start using libraries like redux-thunk or redux-saga to handle asynchronous operations within your actions. redux-thunk is less complicated than redux-saga, but redux-saga empowers you with a lot of cool features yet it could a bit complicated.
You can go big or start small with these libs, either way, they will force you to move you data fetching into your action. If you want support loading of data, then just dispatch actions that tell Redux: "I'm loading data" or "I got an error while loading data" or "I've loaded data". And when that action comes in, update your store, show loader, data or an error if you need. You can take a look on this or that example of using redux-thunk for data fetching, there's everything you need to get started with async actions.
Hope it helps <3
As mentiond as in the other answers, I would setup my reducer only to handle state update and the action creator function to fetch data. Here is the minimal starting point if I were to do it. you can use the loading state to display loading spinners or progress bars.
my action creator function with the individual actions:
export const GET_TASKS_START = "GET_TASKS";
export const GET_TASKS_SUCCESS = "GET_TASKS_SUCCESS";
export const GET_TASKS_FAILURE = "GET_TASKS_FAILURE";
export const getData = () => dispatch => {
dispatch(GET_TASKS_START);
axios
.get("http://localhost:8080/api/taskExecution?&search=&page=1&size=8")
.then(response => {
dispatch({ type: GET_TASKS_SUCESS, payload: response.data });
})
.catch(err => {
dispatch({ type: GET_TASKS_FAILURE, payload: err.response });
});
};
and the reducer would handle the state update as follows:
import {
GET_TASKS_SUCCESS,
GET_TASKS_FAILURE,
GET_TASKS_START
} from "./Action.js";
const initialState = {
tasks: [],
error: null,
loading: false
};
export default function tasksReducer(state = initialState, action) {
switch (action.type) {
case GET_TASKS_START:
return {
...state,
loading: true
};
case GET_TASKS_SUCCESS:
return {
...state,
loading: false,
tasks: action.payload,
error: null
};
case GET_TASKS_FAILURE:
return {
...state,
loading: false,
error: action.payload
};
}
}
I would suggest console logging and checking the responses (data and errors ) and modify the two functions accordingly
We are using redux-promise-middleware and having some difficulty working out what to do about error handling, all errors returned as 400 codes are just handled as 'FULFILLED'. I get that this is probably the correct behaviour but surely there is a way to catch an error in a promise in this setup. As we are not using redux-thunk, I am specifically asking how you would handle say a 400 error being returned from a promise.
our setup is very basic but I'm sure we should be able to
const export doSomething = object => {
const promise = API.doSomething(object)
return {
type: "DO_SOMETHING",
payload: {promise: promise}
}
}
reducer
export default (state = initialstate, action) {
switch(action.type){
case "DO_SOMETHING_FULFILLED":
return action.payload
case "DO_SOMETHING_REJECTED":
return console.log(action.payload)
default:
return initialstate
}
any help is greatly appreciated.
First of all you don't need to set payload to {promise: promise}
Just put the promise directly
Try this for first
export const doSomething = object => {
return {
type: "DO_SOMETHING",
payload: API.doSomething(object)
}
}
This is just a little decoration suggestion(not obligated)
export const doSomething = object => ({
type: "DO_SOMETHING",
payload: API.doSomething(object)
})
Now the answer:
First of all you need check if API.doSomething(object) throws an Error
Or returns Promise.reject() on case of 400 respond code
If it doesn't you need to make it throw
if(res.code === 400) {
throw new Error()
}
// Or just use a library like axios
Promise doesn't know what happened inside you code,
in order to get to DO_SOMETHING_REJECTED
Is to make the API.doSomething(object) throw in 400