I've a simple app built on redux-toolkit. I am dispatching createProduct actions which is working fine. I want to navigate to /products/ page form /products/new page after createProduct action. How can I use navigate (react-router-dom) to do this.
I tried this inside action but failes
[createProduct.fulfilled]: (state, { payload }) => {
toast.success('Product Created Successfully!');
const navigate = useNavigate()
navigate('/products')
return {
...state,
loading: false,
products: state.products ? [...state.products, payload.product] : [payload.product]
};
},
I also tried passing navigate to payload but I encountered this error :
You may not call store.getState() while the reducer is executing. The reducer has already received the state as an argument. Pass it down from the top reducer instead of reading it from the store.
I am dispatching createProduct like this
const handleSubmit = async () => {
console.log('formik.values', formik.values);
dispatch(
createProduct({
...formik.values,
category: formik.values.category._id,
subCategory: formik.values.subCategory._id
})
)
};
Reducer functions are pure functions, you can't issue the navigation action from the reducer, but you can from the asynchronous action or in the calling component. React hooks are also only valid in React functions or custom hooks.
Asynchronous actions return a Promise. You can chain from the resolved Promise, or await it, and issue the imperative navigation.
const navigate = useNavigate();
Using Promise chain:
const handleSubmit = () => {
dispatch(createProduct({
...formik.values,
category: formik.values.category._id,
subCategory: formik.values.subCategory._id
}))
.then(() => {
navigate('/products');
});
};
or async/await:
const handleSubmit = async () => {
try {
await dispatch(createProduct({
...formik.values,
category: formik.values.category._id,
subCategory: formik.values.subCategory._id
}));
navigate('/products');
} catch(error) {
// handle any rejections/errors
}
};
Related
I am trying to understand the logic of how to write unit tests. But i am stuck in here. I have a simple product listing page and it sends requests based on filtering options and returns related products. getProducts function pulls related products and updates my global state. How can i test this? How should be the logic?
getProducts Function
const getProducts = async () => {
const { color, brand, sort, search, page } = state;
const URL = `/api/v1/products?page=${page}&color=${color}&brand=${brand}&sort=${sort}&search=${search}`;
dispatch({ type: SETUP_PRODUCTS_BEGIN });
try {
const { data } = await axios(URL);
dispatch({
type: SETUP_PRODUCTS_SUCCESS,
payload: {
products: data.products,
colorOptions: data.colorOptions,
brandOptions: data.brandOptions,
numOfPages: data.numOfPages,
},
});
} catch (error) {
console.log(error);
}
};
Reducer
if (action.type === SETUP_PRODUCTS_BEGIN) {
return { ...state, isLoading: true };
}
if (action.type === SETUP_PRODUCTS_SUCCESS) {
return {
...state,
isLoading: false,
products: action.payload.products,
colorOptions: action.payload.colorOptions,
brandOptions: action.payload.brandOptions,
numOfPages: action.payload.numOfPages,
};
}
initialState
const initialState = {
products: [],
isLoading: false,
colorOptions: null,
brandOptions: null,
numOfPages: null,
};
Your question is too general.
You have mentioned two components in your question:
Product list React Component
A redux state with some reducers
In order to unit-test components, you need to be able to control all the external dependencies. All your code component/modules should have only one responsibility.
Let's analyze what you have:
Your UI component has too much knowledge - it knows how to get data from the back end, and it even knows the path of the API. Your component has to know how to render based on props/redux state and what actions to dispatch on certain events.
Unfortunately, if you use useDispathc and useElector redux hooks which menans you always have to test your component along with a redux state.
Here are my recomendations. (examples are using jest)
You need to unit test your reducer(s). Since they are pure javascript functions - we do not need to mock anything
import reducer from "/path-to-the-reducer";
describe("Action SETUP_PRODUCTS_BEGIN", () => {
it("Should set isLoading to true nomatter what", () => {
//setup
const initialState = { abc: "Germany", isLoading: undefined };
//act
cons resultState = reducer(initialState, { type: SETUP_PRODUCTS_BEGIN });
//assert
expect(resultState.isLoading).toBe(true);
});
})
In order to test your component you need two things:
2.1 Separate the rest api call into a separate module. In this case you will allow other UI components to reuse same logic for the API calls. This is very basic example:
RestApi.js
const listProducts = async (input) => {
const { color, brand, sort, search, page } = input;
const URL = `/api/v1/products?page=${page}&color=${color}&brand=${brand}&sort=${sort}&search=${search}`;
const axiosResult = await axios(URL);
return axios.data;
}
2.2 You need to use a library to test you react component. As of writing this post react recommends React testing library React testing.
So you need to do a few things:
mock the RestApi methods you need
mock the redux state and "listen for dispatches"
ensure your component renders correct things based on the props/reduxState
I will post some pseudo javascript code to how it should look
import { listProducts } from './restAPI'
//mockign the listProducts to return empty array each time
jest.mock('./restAPI', () => ({
listProducts : () => Promise.resolve([]),
}))
describe("Component initialization ", async () => {
it("Should call SETUP_PRODUCTS_BEGIN and SETUP_PRODUCTS_SUCCESS on componend load", () => {
/// configureStore from the #reduxjs/toolkit npm package
const store = configureStore({ reducer: {}, { isLoading: true, products: [] } })
const renderResult = await render(<Provider store={store}><ProductListComponent /></Provider>);
//assert
const actions = store.getActions();
expect(actions).toHaveLength(2);
expect(actions[0].type).toEqual("SETUP_PRODUCTS_BEGIN");
expect(actions[1].type).toEqual("SETUP_PRODUCTS_SUCCESS");
});
});
Redux in unit testing
I am having difficulty updating my store after calling an API. I am using reduxjs/toolkit. Here is the structure of the project:
react/
store/
api/
dataconsumer/
dataSlice.js
notifications/
notificationsSlice.js
app.js
Here, api contains non-component API calls to the server. They are bound to thunk functions within dataSlice and a successful query updates data just fine.
The following are relevant parts to my reducers.
notificationSlice.js
const slice = createSlice({
...,
reducers: {
// need to call this from api
setNotifications: (state, action) => state.notification = action.payload
}
})
dataSlice.js
export const fetchInitialData = createAsyncThunk(
'chart/fetchInitialData',
async (data) => {
return API.candles.initialData({
...data
})
}
const slice = createSlice({
...
extraReducers: {
...
[fetchInitialData.success]: (state, action) => state.action = action.payload
}
})
And the api
const fetchInitialData = () => {
return fetch(url, {
...
}).then(data => data.json())
.then(data => {
if(data.status === 200) { return data } // works great!
else {
// doesn't work, but functionally what I'm looking for
store.dispatch(setNotifications(data.status))
}
})
}
The problem is when the response is other than 200, I need to update notifications, but I don't know how to get the data to that reducer.
I can't useDispatch because it is outside a component, and if I import the store to my api files it is outside the context provider and my state is uninitialized.
I'm sure I could use localStorage to solve the problem or some other hack, but I feel I shouldn't have to and I'm wondering if there is a key principle I'm missing when organizing my react-redux project? or if there are standard solutions to this problem.
Thanks - I'm new to redux.
Well, if you are using thunk, then the best solution will be to use it in order to dispatch your action after you get the error.
You do it like this:
export const fetchInitialData = () => {
return dispatch => {
...your logic
else {
// now you can dispatch like this
dispatch(setNotifications(data.status))
}
}
};
On the outside it seems not an issue, but when I open the DevTools and then go to network tab. It shows that there are 500 requests made. So how can I refactor the code so this will not happens?
useEffect(() => {
const fetchData = async () => {
try {
const response = await axios.get(
"https://raw.githubusercontent.com/XiteTV/frontend-coding-exercise/main/data/dataset.json"
);
dispatch(getData(response.data));
console.log('input');
} catch (err) {
console.log(err);
}
};
fetchData();
}, [dispatch]);
with redux first create a function which will push your request data into redux state like that outside your react component
export const getMyData = () => {
return async (dispatch) => {
const response = await axios.get(
"https://raw.githubusercontent.com/frontend-coding-exercise/main/data/dataset.json"
);
dispatch(getData(response.data));
}
}
create a function that will extract data from redux. state is redux current state, ownProps is your component props, like <Component customProp={1}/>
const mapStateToProps = (state: any, ownProps: any) => {
return {
...ownProps,
myProps: state.user
};
};
In your connect Function pass the function which will store your data in redux state
export default connect(mapStateToProps, { getMyData })(MyReactComponent);
that way you'll be able to access your data via your component props and also access your getMyData function and also the redux state you mapped to props
props.getMyData();
I'm trying to dispatch a function from a component which has route 'localhost:8080/location/:id' but it seems to give an error, the same dispatch function is working in other component which doesn't have param id.
This is error i'm getting while dispatching that action.
Uncaught TypeError: (0 , _bandwidth2.default) is not a function
This is my dispatchToProps.
const mapDispatchToProps = (dispatch) => ({
addBandwidth: (bandwidth) => dispatch(addBandwidth(bandwidth))
});
export default connect(undefined, mapDispatchToProps)(UpgradeBandwidth);
This is my action.
export const addBandwidth = (bandwidth) => ({
type: 'ADD_BANDWIDTH',
bandwidth
});
Just to clarify, this function working in other component but not in one which has param id in its route. Please help. Thanks.
This is where i called.
handleSubmit = (e) => {
e.preventDefault();
console.log(this.state)
this.props.addBandwidth({
_id: uuid(),
leasedCircuitId: this.state.leasedCircuitId,
bandwidth: this.state.bandwidth,
bandwidthUnit: this.state.bandwidthUnit,
bandwidthType: this.state.bandwidthType,
lastMile: this.state.lastMile,
remarks: this.state.remarks,
discountPolicy: this.state.discountPolicy,
status: false
});
}
Check your App.js
Are you shure you imported addBandwidth()?
import { addBandwidth } from 'path to your action'
The result I want is my component to not render unless all the async function have dispatched. I'm using this as a wrapper to make sure everything has dispatched. I've tried two ways:
call everything in componentWillMount and use setState to set loaded = true. I can then render the component based on my state's loaded key.
ajax = async () => {
try{
const { dispatch } = this.props;
dispatch(loadPack('ars'));
dispatch(loadPack('acr'));
await dispatch(loadProds());
await dispatch(loadRepls());
await dispatch(checkEligibile());
}catch (e) { console.log(e)}
}
componentWillMount() {
this.ajax().then(() => {
this.setState({ loaded: true });
});
}
render() {
const { loaded } = this.state;
return loaded ? <Component/> : null;
}
This gets the desired results, except I see this error:
ExceptionsManager.js:71 Warning: Can only update a mounted or mounting
component. This usually means you called setState, replaceState, or
forceUpdate on an unmounted component. This is a no-op.
I tried dispatching in mapDispatchToProps. Ideally loaded should return true and I should see this.props.loaded = true to load my component. However, I'm receiving a Promise and not the result.
I'm feeling stuck here and not sure what else to try. Any suggestions?
const loadAsync = async dispatch => {
dispatch(loadPack('ars'));
dispatch(loadPack('acr'));
await dispatch(loadProds());
await dispatch(loadRepls());
await dispatch(checkEligibile());
return true
};
export const mapDispatchToProps = dispatch => ({
loaded: loadAsync(dispatch),
});
Since you are using redux, you have a global redux state. So after all dispatch, dispatch one more action that toogle a reducer state to true which indicate that all the actions has been dispatched.
In component, use dispatchStateToProps method to convert reducer state into props and then use that prop to check weather all the actions has been dispatched or not. It should roughly look something like this
ajax = async () => {
try{
const { dispatch } = this.props;
dispatch(loadPack('ars'));
dispatch(loadPack('acr'));
await dispatch(loadProds());
await dispatch(loadRepls());
await dispatch(checkEligibile());
// Add one more action here
await dispatch(everythingDispatched());
}catch (e) { console.log(e)}
}
Then a reducer state that keep track of that
dispatchTrackerReducer.js
switch(action.type){
case "everythingDispatched" :
everythingDispatched: true
break;
}
Then in component use mapStateToProps like this
render() {
const { everythingDispatched } = this.props;
return everythingDispatched ? <Component/> : null;
}
function mapStateToProps(state){
return {
everythingDispatched:state.dispatchTrackerReducer.everythingDispatche
}
}