I have a redux state of the following form, which is managed in slices using combineReducers:
interface AppState {
foos: Foo[];
bars: Bar[];
bazs: Baz[];
}
These are related in the following way:
One Foo has many Bar. One Bar has many Baz. Their structures are as follows:
interface Foo {
id: string;
name: string;
}
interface Bar {
id: string;
name: string;
fooId: string;
}
interface Baz {
id: string;
name: string;
barId: string;
}
I have the regular thunks/actions setup for each part of the state, i.e DELETE_FOO_REQUEST, DELETE_FOO_FAILURE DELETE_FOO_SUCCESS and other CRUD options for each of the entities.
My delete foo thunk looks like this:
function deleteFoo(fooId) {
return async (dispatch, getState) => {
dispatch(deleteFooRequest());
await api.deleteFoo(fooId);
dispatch(deleteFooSuccess(fooId);
// omitted error handling for brevity
}
}
The thing is: when I delete a Foo on my api/backend it also deletes all related Bars and Bazs. Now how do I handle this while using redux-thunk conventions?
Do I create more actions of the form DELETE_BARS_FOR_FOO and dispatch those in the same thunk? Or do I reuse DELETE_BAR_SUCCESS and use it in a loop?
Option A
function deleteFoo(fooId) {
return async (dispatch, getState) => {
dispatch(deleteFooRequest());
await api.deleteFoo(fooId);
const barIds: string[] = selectBarsForFoo(fooId);
dispatch(deleteFooSuccess(fooId);
dispatch(deleteBarsForFoo(fooId);
for (const barId of barIds) {
dispatch(deleteBazForBar(barId));
}
// omitted error handling for brevity
}
}
Option B
function deleteFoo(fooId) {
return async (dispatch, getState) => {
dispatch(deleteFooRequest());
await api.deleteFoo(fooId);
const barIds: string[] = selectBarsForFoo(fooId);
dispatch(deleteFooSuccess(fooId);
for (const barId of barIds) {
dispatch(deleteBarSuccess(barId));
}
// followed by a similar loop for the bazs of each bar
// omitted error handling for brevity
}
}
In option B case I am reusing an action meant for something else technically. In both actions I'm dispatching in a loop which would impact performance as well. I am using react-redux however and can use the batch() api so no worries there.
Are these my only two options while using redux-thunk or is there a superior/conventional way of going about this?
Instead of complicating your actions is it an option to just deal with this in your reducer (as suggested in comments). The following way you can still use combineReducers but have a combined reducer that gets {foo,bar,bas} as state:
const fooBarBaz = combineReducers({
foo:fooReducer,
bar: barReducer,
baz:bazReducer,
});
const combined = (state,action)=>{
//handle foo remove success action, state is {foo,bar,baz}
}
export default function reducer(state,action){
return combined(fooBarBaz(state,action),action)
}
Related
I'm using Typescript, and I know how to use useQuery hook with variables, but now I have a GraphQL query without variables like below:
const GetTopAlertsQuery = gql`
query getTopAlerts {
getTopAlerts {
ens
walletAddress
}
}
`;
Basically I just need it return all the data in the database without doing any filtering. I have already implemented the back-end and it works successfully, so the query should be good.
I also have set up these two interfaces to hold the data:
interface topAlertValue {
ens: string;
walletAddress: string;
}
interface jsonData {
topalerts: topAlertValue[];
}
And I have tried the below ways, but none of them work:
// attempt #1
const { data } = useQuery<jsonData>(
GetTopAlertsQuery
);
// attempt #2
const data = ({ topalerts }: jsonData ) => {
useQuery(GetTopAlertsQuery);
};
// attempt #3
const data = <Query<Data, Variables> query={GetTopAlertsQuery}>
{({ loading, error, data }) => { ... }}
</Query>
If you know how to use useQuery hook without variables, please help me out! Thanks!
For example, my app contains the two list: colors & my favorite colors. How to create the re-usable filter-module for this two lists?
The problem is the actions in redux commiting into global scope, so filter-reducer for colors and filter-reducer for favorite colors reacting to the same actions.
I try something like high-order functions that receive the module-name and returned new function (reducer) where classic switch contain module + action.type.
But how make scoped actions or scoped selectors? Best-practise?
Maybe Redux-Toolkit can solve this problem?
High-order reducer
High-order action
High-order selector
Well described here
Name your function like createFilter and add a parameter like domain or scope
after this in switch check it e.g
export const createFilters = (domain, initState) => {
return (state = initState, { type, payload }) => {
switch (type) {
case: `${domain}/${FILTER_ACTIONS.ADD_FILTER}`:
...
and for actions create something like this
const createFilterActions = (domain: string) => {
return {
addFilter: (keys: string[]) => {
return {
type: `${domain}/${FILTER_ACTIONS.ADD_FILTER}`,
payload: keys
}
},
updateFilter: (key: string, state: any) => {
return {
type: `${domain}/${FILTER_ACTIONS.FILTER_UPDATED}`,
payload: { key, state }
}
},
}
}
So I'm creating my first ReactJS/redux application and I need a little guidance.
I've created a generic apiFetch<T>(method, params) : Promise<T> function which lives in api/apiClient.ts. (Not a React component, but called indirectly from React components)
Basically every fetchEmployee/fetchSettings/fetchWhatever method in rpc/rpcMethods.ts calls this apiFetch<T>() function.
What I'd like to achieve is a statusbar in my app which shows how many concurrent api calls are active. I therefore created a redux rpcStatusSlice based on this redux example.
Can I make apiFetch<T>() update the slice without passing the UseAppDispatch() result as a parameter from my React components?
If I directly import the store in apiClient.ts and call the state modifying functions from rpcStatusSlice on it I get this error:
Uncaught ReferenceError: Cannot access '__WEBPACK_DEFAULT_EXPORT__' before initialization
at Module.default (bundle.js:1444:42)
at Module../src/store/store.ts (bundle.js:1957:67)
at Module.options.factory (bundle.js:90091:31)
at __webpack_require__ (bundle.js:89541:33)
at fn (bundle.js:89762:21)
at Module../src/api/apiClient.ts (bundle.js:206:70)
at Module.options.factory (bundle.js:90091:31)
at __webpack_require__ (bundle.js:89541:33)
at fn (bundle.js:89762:21)
at Module../src/api/rpcMethods.ts (bundle.js:288:68)
apiFetch.ts:
import { store } from "../store/store";
import { incrementByAmount } from "../store/features/rpcStatusSlice";
export function apiFetch<T>(method: string, params: any): Promise<T> {
store.dispatch(incrementByAmount(1));
return fetch(apiUrl, {
method: "POST",
cache: "no-cache",
mode: "cors",
redirect: "follow",
body: JSON.stringify(getApiRequest(method, params)),
})
.then(etc)
./store/features/rpcStatusSlice.ts
import { createSlice, PayloadAction } from '#reduxjs/toolkit';
import { RootState } from '../store';
export interface ActiveRequest{
requestType: string;
}
export interface RpcStatus {
activeRequestsCount: 0;
activeRequests: ActiveRequest[];
}
export interface RpcStatusState {
value: RpcStatus;
status: 'idle' | 'loading' | 'failed';
}
const initialState: RpcStatusState = {
value: {
activeRequestsCount: 0,
activeRequests: []
},
status: 'idle',
};
export const rpcStatusSlice = createSlice({
name: 'rpcstatus',
initialState,
// The `reducers` field lets us define reducers and generate associated actions
reducers: {
increment: (state) => {
// Redux Toolkit allows us to write "mutating" logic in reducers. It
// doesn't actually mutate the state because it uses the Immer library,
// which detects changes to a "draft state" and produces a brand new
// immutable state based off those changes
state.value.activeRequestsCount += 1;
},
decrement: (state) => {
state.value.activeRequestsCount -= 1;
},
// Use the PayloadAction type to declare the contents of `action.payload`
incrementByAmount: (state, action: PayloadAction<number>) => {
state.value.activeRequestsCount += action.payload;
},
},
});
export const { increment, decrement, incrementByAmount } = rpcStatusSlice.actions;
// The function below is called a selector and allows us to select a value from
// the state. Selectors can also be defined inline where they're used instead of
// in the slice file. For example: `useSelector((state: RootState) => state.rpcstatus.value)`
export const selectCount = (state: RootState) => state.rpcStatus.value.activeRequestsCount;
export default rpcStatusSlice.reducer;
./store/store.ts
import { configureStore, ThunkAction, Action } from '#reduxjs/toolkit';
import rpcStatusReducer from './features/rpcStatusSlice';
export const store = configureStore({
reducer: {
rpcStatus: rpcStatusReducer
},
});
export type AppDispatch = typeof store.dispatch;
export type RootState = ReturnType<typeof store.getState>;
export type AppThunk<ReturnType = void> = ThunkAction<
ReturnType,
RootState,
unknown,
Action<string>
>;
Can I make apiFetch() update the slice without passing the UseAppDispatch() result as a parameter from my React components?
This sounds like you want apiFetch() to do two things:
Make an HTTP request.
Update redux state.
This violates the separation of concerns principle. Instead, call apiFetch() from a thunk that then updates redux state.
If your StatusBar component is the only consumer of the number of concurrent api calls, I'd consider not even using redux for this. This counter is not necessarily even global state, I'd see it as volatile, local component state for the StatusBar. Therefore, the solution I'm proposing relies only on React. It takes advantage of the pattern of keeping state in the module scope of the apiClient. At runtime, your apiClient module exists as a singleton with its own scope (many modules importing the apiClient will always use the same instance).
This abstracts the task of counting api calls away and lets the other parts of the codebase use apiFetch() without worrying about the counter. No need to adjust your existing thunks etc..
// apiClient.ts
let incrementCounter = (): void => null;
let decrementCounter = (): void => null;
export const onRequestStart = (callback: () => void) => {
incrementCounter = callback;
};
export const onRequestEnd = (callback: () => void) => {
decrementCounter = callback;
};
export const apiFetch = (method, params) => {
incrementCounter();
// do actual api call
return someKindOfPromise.finally(decrementCounter);
};
// StatusBar.tsx (component showing the number of concurrent api calls)
import { onRequestStart, onRequestEnd } from 'apiClient.ts';
const StatusBar = () => {
const [numConcurrentCalls, setNumConcurrentCalls] = useState(0);
useEffect(() => {
// Tell api client to always call the following function when a request starts.
onRequestStart(() => setNumConcurrentCalls(num => num + 1));
// Tell api client to always call the following function when a request ends.
onRequestEnd(() => setNumConcurrentCalls(num => num - 1));
}, [setNumConcurrentApiCalls]);
return (
<div>
<p>Concurrent api calls: {numConcurrentCalls}</p>
</div>
);
};
What I'm trying to achieve:
I have a NextJS + Shopify storefront API application. Initially I set up a Context api for the state management but it's not that efficient because it re-renders everything what's wrapped in it. Thus, I'm moving all state to the Redux Toolkit.
Redux logic is pretty complex and I don't know all the pitfalls yet. But so far I encounter couple problems. For example in my old Context API structure I have couple functions that take a couple arguments:
const removeFromCheckout = async (checkoutId, lineItemIdsToRemove) => {
client.checkout.removeLineItems(checkoutId, lineItemIdsToRemove).then((checkout) => {
setCheckout(checkout);
localStorage.setItem('checkout', checkoutId);
});
}
const updateLineItem = async (item, quantity) => {
const checkoutId = checkout.id;
const lineItemsToUpdate = [
{id: item.id, quantity: parseInt(quantity, 10)}
];
client.checkout.updateLineItems(checkoutId, lineItemsToUpdate).then((checkout) => {
setCheckout(checkout);
});
}
One argument (checkoutId) from the state and another one (lineItemIdsToRemove) extracted through the map() method.
Inside actual component in JSX it looks and evokes like this:
<motion.button
className="underline cursor-pointer font-extralight"
onClick={() => {removeFromCheckout(checkout.id, item.id)}}
>
How can I declare this type of functions inside createSlice({ }) ?
Because the only type of arguments reducers inside createSlice can take are (state, action).
And also is it possible to have several useSelector() calls inside one file?
I have two different 'Slice' files imported to the component:
const {toggle} = useSelector((state) => state.toggle);
const {checkout} = useSelector((state) => state.checkout);
and only the {checkout} gives me this error:
TypeError: Cannot destructure property 'checkout' of 'Object(...)(...)' as it is undefined.
Thank you for you're attention, hope someone can shad the light on this one.
You can use the prepare notation for that:
const todosSlice = createSlice({
name: 'todos',
initialState: [] as Item[],
reducers: {
addTodo: {
reducer: (state, action: PayloadAction<Item>) => {
state.push(action.payload)
},
prepare: (id: number, text: string) => {
return { payload: { id, text } }
},
},
},
})
dispatch(todosSlice.actions.addTodo(5, "test"))
But 99% of the cases you would probably stay with the one-parameter notation and just pass an object as payload, like
dispatch(todosSlice.actions.addTodo({ id: 5, text: "test"}))
as that just works out of the box without the prepare notation and makes your code more readable anyways.
I have a simple state machine that handles an input form
export const chatMachine = Machine({
id: 'chat',
initial: 'idle',
states: {
idle: {
on: {
SET_MESSAGE: { actions: ['handleMessageChange'] },
COMMENT_SUBMITTED: {
actions: ['submitComment']
}
}
}
}
});
I would like the submitComment action to fire off a function and then reset a field in context like this:
submitComment: (ctx, e) => {
e.payload(ctx.message);
assign({
message: ''
});
}
This doesn't work.
It fires the method I'm passing in but it doesn't make it to the assign bit.
Can I do two thing sin one action or should I be creating two seperate actions?
You should be creating two separate actions because those are two separate actions.
I'm not sure what e.payload(ctx.message) does, but events should be purely data - you should not put functions in events.
Also, assign(...) is not imperative. It is a pure function that returns an action that looks something like { type: 'xstate.assign', ...}. None of XState's actions are imperative.
Try this:
// ...
COMMENT_SUBMITTED: {
actions: ['submitComment', 'assignComment']
},
// ...
actions: {
submitComment: (ctx, e) => { ... },
assignComment: assign({ message: '' })
}