Generate reducers dynamically (universal reducer for api calls) - reactjs

In redux how can i make reducers dynamically based on api call passed as string to an action-creator to reduce the boilerplate (so for each api call there was a dedicated key inside the store)?
And should i even try to do that?
Example jsfiddle
The problem is here:
export function universalFetchReducer(state = initialState, action) {
switch (action.type) {
case 'FETCHING_DATA' + action.metadata:
return {
...state,
isFetching: true
};
case 'FETCHING_DATA_SUCCESS' + action.metadata:
return {
...state,
isFetching: false,
data: action.data,
dataFetched: true
};
case 'FETCHING_DATA_FAILURE' + action.metadata:
return {
...state,
isFetching: false,
error: true
};
default:
return state;
}
}
For now i can create actions and their names based on url passed to an action-creator, but cannot make a dedicated reducer.

Solved this by using redux-injector, followed its api to create an action creators and a simple async action creator (axios used):
export function getData(api) {
return {
type: `FETCHING_DATA_${api}`,
meta: api
}
}
export function universalFetchData(api) {
injectReducer(`universalFetch${api}`, universalFetchReducer);
return dispatch => {
dispatch(getData(api)) //Some initial action. Pass api to name actions
axios
.get(api)
.then(response => {
dispatch(getDataSuccess(response.data, api)) //Some success action
})
.catch(error => getDataFailure(error.response.status, api)) } } //Some failure action
Then just fired an universalFetchData('path_to_api') from component and got FETCHING_DATA_path_to_api action in redux-devtools.
Got data from store
state.universalFetchReducer_path_to_api
and passed this state to render with e.g. ramda's pathOr to set unkown initial state.
Lesson learned: you will be able to make many simple lazy loading api calls fast, but do this only if you know what data you're getting. For more dangerous logic use regular reducers upfront. This solution nowhere near acceptable but it gets job done.

Related

attach a loading component on submit using react

I have a work around that I have been trying to attach a loader or progress bar component after clicking the submit form. I have the loading declared in redux initial state but don't know how to proceed from here.
Yes, I know how to attach the loading component while fetching data from an API using GET request but don't know how to utilize with POST request. So, the form should load a component which clicking to submit and then it will send the data to the store.
Following is the sandbox link:
https://codesandbox.io/s/brave-lewin-bp3w6?file=/src/Form.jsx
Any help would be highly appreciated.
Create an additional action startLoading that you can dispatch before the API call:
export const START_LOADING = "START_LOADING";
export const startLoading = () => {
return {
type: START_LOADING
};
};
export default function(state = initialState, action) {
switch (action.type) {
case START_LOADING:
return {
...state,
loading: true
};
case ADD_USERS:
return {
...state,
users: [...state.users, action.payload],
loading: false
};
default:
return state;
}
}
Then in your onSubmit you can do:
props.startLoading();
props.addUsers(values);

How to share/split a Redux Store between multiple generic components?

I have a generic component called "VendorResults". I am passing a string prop down to each of these generic components such as "Microsoft", "Apple", etc.
<ScrollView>
<SearchResults/>
<VendorResults vendor={"microsoft"}/>
<VendorResults vendor={"oracle"}/>
</ScrollView>
Within this generic component, I am passing the vendor prop as a parameter to my Redux-Thunk actions as such:
componentDidMount() {
const {vendor} = this.props;
this.props.getVendorInformation(vendor);
}
An API call kicks off, and Thunk actions are dispatched. The data eventually makes its way to the Reducer and store. However, When I have more than one generic Vendor component, whichever async call finishes last, appears to take precedent over all the others. For example, if oracle finishes loading last, the microsoft component's state will change and show oracle data.
Actions
export function getVendorInformation(vendor) {
const url = `${VENDOR_URL}api/search/${vendor}`;
return dispatch => {
dispatch(getVendor());
fetch(url)
.then(blob => blob.json())
.then(data => {
dispatch(getVendorSuccess(data))
})
.catch(e => {
console.log(e);
dispatch(getVendorError(e.message))
});
};
Reducer
export default function(state=initialState, action){
switch (action.type){
case FETCHING_VENDOR: return {payload:[], fetching: true}
case FETCH_VENDOR_SUCCESS: return {payload: action.payload.data}
case VENDOR_ERROR: return {payload:[], error: true, ...state}
}
return state;
}
My Question:
I want to maintain this pattern of generic/reusable Vendor components - I do not want a new component for each vendor. The same goes for actions/reducers; unique vendor actions/reducers would get messy.
How can I share/split/partition a single Redux store into vendor specific chunks to maintain seperation of state but still benefit from one flow. Thank you!!
You need to pass vendor to reducer via action and re-do structure of your state. If list of vendors is pre-determined and not very long, it probably will be less messy to just create separate actions/reducers.
Otherwise, you need to have nested reducer:
const supportedActions = [FETCHING_VENDOR, FETCH_VENDOR_SUCCESS, VENDOR_ERROR];
const initialVendorState = {data:[], fetching: false, error: false};
const vendorReducer = (state = initialVendorState, action) => {
switch (action.type){
case FETCHING_VENDOR: return {data:[], fetching: true}
case FETCH_VENDOR_SUCCESS: return {data: action.payload.data}
case VENDOR_ERROR: return {...state, data:[], error: true}
}
return state;
}
const reducer = (state = {}, action) => {
if (supportedActions.includes(action.type)) {
const s = {};
s[action.payload.vendor] = vendorReducer(state[action.payload.vendor], action);
return {
...state,
...s
};
}
return state
}
export default reducer;
And your action creators should take vendor as parameter and pass it to reducer:
const fetchVendorSuccess = (vendor, data) => ({
type: FETCH_VENDOR_SUCCESS,
payload: {
vendor,
data
}
});
In your connect function you will need to use smth like data: (state[vendor] || {}).data to avoid errors if state does not have any info about that vendor
However, When I have more than one generic Vendor component, whichever async call finishes last, appears to take precedent over all the others. For example, if oracle finishes loading last, the microsoft component's state will change and show oracle data.
You are seeing Oracle data because after fetching the vendor data you are overwriting the entire vendor state with the latest array of vendor items.
case FETCH_VENDOR_SUCCESS: return {payload: action.payload.data}
To avoid this, you would need to merge the previous state with the new state.
Solution depends on what each vendor response looks like. As Gennady suggested, you can use an object and make each vendor a property on the object.
Using a flat array to store all the different vendor items presents challenges. How would you determine if a vendor has already been fetched?
To avoid overwriting the previous vendor, you would need to merge the new state with previous state. E.g.
case FETCH_VENDOR_SUCCESS: return [...state.data, ...payload.data]

How can I watch for changes from reducer in react component?

I have react app, and I am using redux as a store. Along with redux i am using redux-thunk. For example, i have action getUsers that fetch all users and storing them in user reducer. Also, if there is some error while fetching them i store that error. My question is how to in react component named UsersOverwiew watch for changes happend in reducer, for example error, and show that error to user? I did it with useEffect hook, but is there better way?
User reducer
case GET_USERS_BEGIN:
return {
...state,
users: {
loading: true,
error: {},
data: []
}
};
case GET_USERS_SUCCESS:
return {
...state,
users: {
loading: false,
error: {},
data: action.users
}
};
case GET_USERS_FAILURE:
return {
...state,
users: {
...state.users,
loading: false,
error: action.error,
}
};
UserOverview component
// fetching users
useEffect(() =>{
getUsers();
}, []);
// watch for changes in user reducer
useEffect(() =>{
if(users.error){
// if error happend do something
}
}, [users]);
This is only small part of code, i have already everything connected, component and reducer, but i wanted to simplify it as much as i can.
You are doing it the correct way if you are using functional components. The above answer which suggests componentWillMount() requires class based components.
However doing it with useEffect is completely fine.

Redux action taking too long add one item to empty array - No Promise returned

I am using Redux to manage the state of my react app. I am creating an object, then passing this object to addTile function which is my action.
So my action.ts looks like this:
export function addTile(tile){
return {
type: "ADD_TILE",
payload: tile
}
}
My reducer.ts looks like this:
const reducer = (state = {
isPanelOpen: false,
isDiscardAllChangesOpen: false,
tiles: [],
tempTiles: [],
}, action) => {
switch (action.type) {
case "PANEL_VISIBILITY":
state = {
...state,
isPanelOpen: action.payload
};
break;
case "ADD_TILE":
state = {
...state,
tiles: [...state.tiles, action.payload]
}
break;
}
return state;
};
export default reducer;
However, if I try to use this in my component like this:
this.props.addTile(tile)
alert(this.props.tiles.length)
The length will be 0. However, the item is really added to the array, but at the time of the alert execution, the length was 0. From my reading on Redux docs, actions by default are async (or at least that's how I understand they are).
I even try to do this:
this.props.addTile(tile)
.then(response => { //some code})
Then I get cannot read property then of undefined.
Any ideas?
When you try to check the prop right after dispatching an action, React has not yet had a chance to re-render. It's not that it's "taking too long", it's that your own code is still executing. So, React has not re-rendered your component, and the prop value is still the same.
Your promise example would only work if addTile() was a thunk that returned a promise.

Redux-promise not resolving a promise when in the action there is a number

I am currently developing an application that is a copy of Instagram. It works with React-Redux, calls an external API via axios to fetch photos that are actually blog posts. I need to also pass the amount of likes (so 0) for each one, that I am adding in my fetchPhotos action creator, which causes my application to crash. This works fine whenever the action creator is only returning type and payload.
When I console logged the action it actually turned out that the promise is now not being resolved and thus followed the error.
Action creator:
export function fetchPhotos() {
const response = axios.get("https://dog.ceo/api/breed/husky/images");
return {
type: FETCH_PHOTOS,
payload: response,
likes: 0
};
}
Reducer:
export default function(state = [], action) {
switch(action.type) {
case FETCH_PHOTOS:
console.log(action);
return [action.payload.data.message, action.likes];
default:
return state;
}
}
In App:
const history = createBrowserHistory();
const store = createStore(
connectRouter(history)(reducers),
compose(applyMiddleware(routerMiddleware(history), ReduxPromise))
);
Is there any way to make the action creator actually resolve this promise inside the action.payload?
For documentation:
As #fshauge mentioned, the payload has to be a promise and adding the property of likes breaks it. I found this in the issues, which has solved my issue. The likes property actually has to go into meta, so the end result that functions correctly is:
export function fetchPhotos() {
const response = axios.get("https://dog.ceo/api/breed/husky/images");
return {
type: FETCH_PHOTOS,
payload: response,
meta: {
likes: 0
}
};
}
According to the documentation of redux-promise, it expects either a promise or a Flux Standard Action (FSA) where the payload is a promise. Since adding the 'likes' property breaks FSA-compliancy, it has to be inside the payload somehow.
I suggest returning a promise directly from the action creator with the 'likes' property embedded as follows:
export async function fetchPhotos() {
const response = await axios.get("https://dog.ceo/api/breed/husky/images");
return {
type: FETCH_PHOTOS,
payload: {
data: response.data,
likes: 0
}
};
}
redux-promise will automatically call dispatch when the promise resolves, and you can use the following code in your reducer:
export default function (state = [], action) {
switch (action.type) {
case FETCH_PHOTOS:
console.log(action);
return [action.payload.data.message, action.payload.likes];
default:
return state;
}
}

Resources