I have some coding experience with React & Redux one and half year ago. And then I have been using Vue & Vuex in this year. I like the simplicity of the Vuex structure. So when I return to React & Redux. I want to try some way to make Redux code structure become more simplify.
I have try to define the State and Action using a single object(like Vuex). Then use reduxGenerator function to generate 'reducer' and 'action creator'.
But I didn't think deeply about this. I always worried about this method will cause some problems.
I know that Redux splits the 'state', 'reducer', 'action type' and 'action creator' to make the structure clearer. The team can work together to write different part of the entire state. But more often in my team. Everyone is assigned a different module. One developer needs to face all 'state', 'reducer', 'action type' and 'action creator' code in multiple place. And the Developer need to manually write some 'mechanized' code. But why developers still maintain this initial Redux code structure all the time. Is my method causing some serious problems?
Example code:
https://codesandbox.io/s/redux-simplify-v07n9
Redux Object
const lists = {
naming: "lists",
initState: [
{
id: idGenerator(),
name: "List 1"
},
{
id: idGenerator(),
name: "List 2"
}
],
actions: {
addList(state) {
return [
...state,
{
id: idGenerator(),
name: "New List"
}
];
},
// ....
}
}
export default reduxGenerator(lists);
reduxGenerator
export default ({ naming, initState, actions }) => {
const redux = {};
redux.actionCreator = Object.fromEntries(
Object.entries(actions).map(action => [
action[0],
args => ({
type: `${naming}/${action[0]}`,
args
})
])
);
redux.reducer = (state = initState, action) => {
console.log(`%c action: ${action.type}`, "color: #00BEBE");
const actionType = /.+\/.+/.test(action.type)
? action.type.split("/")[1]
: action.type;
if (Object.keys(actions).includes(actionType)) {
return actions[actionType](state, action.args);
}
console.error(`${action.type} not found!`);
return state;
};
return redux;
}
So I hope to discuss the rationality of this. To help me jump out of my personal limitations.
This may not be a general question in stackoverflow. Please forgive me.
Related
In my app I have a recurring scenario in which the app shows a feedback screen to the user after some backend command is executed to provide info for the user on the outcome of that request. My particular issue is that many of those scenarios have too many different outcome that needs to be informed to the user.
type Feedback = {
color: 'green' | 'red';
title: string;
subtitle?: string;
primaryButton: {
label: string;
onPress(): void;
};
secondaryButton?: {
label: string;
onPress(): void;
};
}
The component is the same, what changes is the value of the state properties that each feedback has. The component is prepared to produce a variety of options based on the state: e.g., color, title, sometimes subtitle, primary button, sometimes secondary button, etc;
function FeedbackScreen() {
const navigation = useNavigation();
const [feedback, setFeedback] = useState<Feedback>();
useEffect(() => {
doBackendStuff().then(res => {
if (res.success) {
setFeedback({
color: 'green',
//...
primaryButton: {
label: 'Go Home!',
onPress: () => navigation.navigate('Home'),
}
})
} else if (res.errorCode = 'error.code.0') {
setFeedback({
color: 'red',
//...
primaryButton: {
label: 'Go to help center!',
onPress: () => navigation.navigate('Help', { id: 1 }),
}
})
} else if (/*... */) {
//...
}
//...
});
}, []);
return !feedback ? <Loading /> : <FeeedbackView {...feedback} />;
}
So the problem is, as the number of possible feedbacks increase, it becomes unreasonable to maintain all feedbacks in the same place, as I showed on the example code, each feedback is unique, but it depends on the react context to execute hooks functions among other things. In reality, the Feedback type is much more complex than that, but it should be sufficient to illustrate.
My question is [I guess], is there a better way to do that? Is there a pattern to define those states each in their own files? So that it become easier to maintain the code perhaps?
In one of the experiments that I tried, I created a redux like solution to select one feedback from the list of feedbacks possible based on a code and a payload, like redux do with action and a payload as per say, it's was a good approach, helped isolate each possible state if I wanted, but, it required react context to implement the side effects like navigation and collecting function like translations for example.
Anyways, does anybody have a better idea or a known pattern to solve this type o problem? Perhaps there are a totally different approach to it that might be better.
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.
So in typical programming scenarios, if you mutate an object, it mutates the object everywhere. However, in React, since states are immutable and only mutable through each's set*, if states are nested in each other, like the scenario shown below, only the currentMember's name will change, and not the version of currentMember in currentTeam, or teams. I'd have to go through each and mutate them one by one, which is cumbersome.
What would be the best way to mutate multiple states at once, or achieve a similar effect? I've done so by indexing, but it's more cumbersome to work with, and i didnt know if there were a textbook hook that fixes this.
import React, { useState } from 'react'
interface Member {
name: string
}
interface Team {
name: string
members: Member[]
}
export default (props: {}) => {
const [teams, setTeams] = useState<Team[]>([
{
name: 'Team One',
members: [{ name: 'Wyatt' }, { name: 'Michael' }]
}
])
const [currentTeam, setCurrentTeam] = useState<Team>(teams[0])
const [currentMember, setCurrentMember] = useState<Member>(currentTeam.members[0])
return (
<>
<h1>${currentMember.name}</h1>
<button onClick={() => setCurrentMember(currentMember => { ...currentMember, name: 'Zach' })}>
Change current member name to Zach!
</button>
</>
)
}
As mentioned, you are making things a bit complicated by using state this way. You should have one base state that contains all of the teams, then reference the bits that are important to you by index.
For example, your teams state is fine as is. Your currentTeam and currentMember states should be indexes or some other reference to the state within teams you want to map to.
So, in specific terms, I'd change the format of your code here like so (forgive me as I don't write TypeScript, so I'm going to straight vanilla javascript to avoid making typos):
import React, { useState } from 'react'
// interface Member {
// name: string
//}
// interface Team {
// name: string
// members: Member[]
//}
export default (props: {}) => {
const [teams, setTeams] = useState([
{
name: 'Team One',
members: [{ name: 'Wyatt' }, { name: 'Michael' }]
}
])
const [currentTeamIndex, setCurrentTeam] = useState(0)
const [currentMemberIndex, setCurrentMember] = useState(0)
return (
<>
<h1>${teams[currentTeamIndex].members[currentMemberIndex]}</h1>
<button onClick={() => setTeams(teams => ({
// Shallow copy the teams via mapping through them
...teams.map((team, teamIndex) => {
// If the current team index isn't the index we're on right now, then just
// return the existing team in its given place.
if (teamIndex !== currentTeamIndex) return team
// If we're at this point, it means the teamIndex matches the currentTeamIndex
// and we need to mutate this. We'll just do that by returning a new object
return {
...team, // Make sure we don't miss anything
...members.map((member, memberIndex) => {
// Same as the outer map, if the current member index isn't the same as the
// given memberIndex, then just return the member we're on, we're not mutating it
if (memberIndex !== currentMemberIndex) return member
return {
...member,
name: 'Zach'
}
})
}
}
})
>
Change current member name to Zach!
</button>
</>
)
}
As you can see, drilling that far down into an object isn't so simple, but it's possible to do without mutating the original data - you can accomplish what you are looking for by reconstructing the data on the fly with Array functions, spread syntax, and index references in state.
You might also want to consider confining this in a reducer, with a specific action containing the team id index, and member id index to make this quite a bit more composable.
I am implementing Reselect in my project and have a little confusion on how to properly use it. After following multiple tutorials and articles about how to use reselect, I have used same patterns and still somethings dont work as expected.
My selector:
const getBaseInfo = (state) => state.Info;
const getResources = (state) => state.Resources;
export const ASelector = createSelector(
[getBaseInfo, getResources],
(items, resources) => {
let result = {};
for(const item in items) {
console.log(item);
result[item] = _.pick(items[item], ['Title', 'Type', 'Beginning', 'minAmount', 'Address'])
}
for(const item in resources) {
console.log(item);
result[item] = {...result[item], firstImage: resources[item].firstImage}
}
return result;
}
);
mapStateToProps component:
function mapStateToProps(state) {
console.log(state);
return {
gridInfo: ASelector(state)
}
}
Now at first my initial state is:
state = { Info: {}, Resources: {} }
My Reducer:
const Info = ArrayToDictionary.Info(action.payload.data.Info);
const Resources = ArrayToDictionary.Resources(action.payload.data.Info);
let resourcesKeys = Object.keys(Resources);
let infoKeys = Object.keys(Info);
let temp = { ...state };
let newInfo;
for (let item of infoKeys) {
newInfo = {
Title: Info[item].Title,
Type: Info[item].Type,
BeginningOfInvesting: Info[item].BeginningOfInvesting,
DateOfEstablishment: Info[item].DateOfEstablishment,
pricePerUnit: Info[item].PricePerUnit,
minUnits: Info[item].MinUnits,
publicAmount: Info[item].PublicAmount,
minInvestmentAmount: Info[item].MinInvestmentAmount,
EinNumber: Info[item].EinNumber,
Address: Info[item].Address,
Status: Info[item].Status,
Lat: Info[item].Lat,
Lng: Info[item].Lng,
CurrencySymbol: Info[item].CurrencySymbol,
Publicity: Info[item].Publicity
}
temp.Info[item] = { ...temp.Info[item], ...newInfo }
}
for (let item of resourcesKeys) {
temp.Resources[item] = { ...temp.Resources[item], ...Resources[item] }
}
return temp;
As a component renders with the initial state, I have an action pulling data from api and saving it accordingly into the state inside reducers.
Now my state is changed, but after debugging a little into reselects code, I found in the comparison function that the old and new states are the same.
Suddenly my "old" state became already populated with the newState data and it of course failing the comparison as they became the same.
Is there anything wrong with my selectors?
I have really tried to use it as the documentation states, but still cant understand how to solve my little issue.
Thank you very much for reading and helping!
It looks like the temp.Info[item] and temp.Resources[item] lines are mutating the existing state. You've made a shallow copy of the top level, but aren't correctly copying the second level. See the Immutable Update Patterns page in the Redux docs for an explanation of why this is an issue and what to do instead.
You might want to try using the immer library to simplify your immutable update logic. Also, our new redux-starter-kit library uses Immer internally.
Let's say I have a reusable container. It is a wizard with multiple pages.
The wizard state is driven by redux/actions. When an action is fired, I use a reducer to update my state.
What if I want to have multiple wizards duplicated, with it's own state?
I am thinking there must be a way to have actions handled by a certain dynamic reducer (that can be created/destroyed), and then have each individual wizard driven from these dynamic pieces of the store/state.
Is this recommended? Are the libraries out there that make this easier?
Just partition your main state into as many wizard states as you need and send a wizard id along with each action so that your reducer knows which one to tackle.
As Array
{
wizards: [
{ id: 'A', state: true },
{ id: 'B', state: false },
{ id: 'C', state: true }
]
}
You can write a wizard reducer which understands how to reduce a single wizard state.
function wizardReducer(wizard, action) {
switch(action) {
case 'TOGGLE':
return {
id: wizard.id,
state: !wizard.state
};
default:
return wizard;
}
}
Then write a wizardsReducer which understands how to reduce a list of wizards.
function wizardsReducer(wizards, action) {
return wizards.map(function(wizard) {
if(action.id == wizard.id) {
return wizardReducer(wizard, action);
} else {
return wizard;
}
});
}
Finally, use combineReducers to create a root reducer which delegates responsibility for the wizards property to this wizardsReducer.
combineReducers({
wizards: wizardsReducer
});
As Object
If you're storing your wizards in an object instead, you'll have to construct your wizardsReducer slightly differently.
{
wizards: {
A: { id: 'A', state: true },
B: { id: 'B', state: false },
C: { id: 'C', state: true }
}
}
It wouldn't make much sense to map over the states, when we can just select the state we need straight away.
function wizardsReducer(wizards, action) {
if(!(action.id in wizards)) return wizards;
const wizard = wizards[action.id];
const updatedWizard = wizardReducer(wizard, action);
return {
...wizards,
[action.id]: updatedWizard
};
}
The OP asked for a lib for this, so I am just throwing it here.
I created infra functions which will intercept the actions and add meta-data to each action. (following FSA)
You can use this easily to create multiple containers without them affecting each other.
reducer-action-interceptor