useReducer - Enforce only one true value without explicitly setting others false? - javascript

I'm looking for a smarter way to enforce
one value true, setting the others to false
This Python q&a suggests an enum
(I'm not familiar with either)
I can already
switch (action.type) {
case 'loadCurrent':
return {...state,
loadCurrent: true,
loadPrev: false, etc ...};
But this is a lot of boilerplate and intuition (as well as that linked python question) tell me there's a more elegant solution.
function reducer (state, action) {
switch (action.type) {
case 'loadCurrent':
return {...state, loadCurrent: true};
case 'fetchNew':
return {...state, fetchNew: true};
case 'loadPrevHook':
return {...state, loadPrevHook: true };
case 'loadNextHook':
return {...state, loadNextHook: true };
case 'loaded':
return {...state, loaded: true };
}
}
const initialState = {
loadCurrent: false,
fetchNew: false,
loadPrevHook: false,
loadNextHook: false,
loaded: true }

Having separate properties for these doesn't make the most sense. Put all the data into a single property instead - perhaps call it loadStage or whatever you think is appropriate.
If, as the code here suggests, the state doesn't have any other properties, just setting the action.type looks like it'd do.
function reducer (state, action) {
return { loadStage: action.type };
}
If you expanded the reducer to include other actions as well not related to the loading actions, you could make an array corresponding to the loading actions.
const loadStageActions = ['loadCurrent', 'fetchNew', 'loadPrevHook', 'loadNextHook', 'loaded'];
function reducer (state, action) {
if (loadStageActions.includes(action.type)) {
return { ...state, loadStage: action.type };
}
// implement other logic here
}
Then, in the rest of your code, just check the one string property instead of the multiple booleans. Eg instead of doing state.loadCurrent, do state.loadStage === 'loadCurrent'.

Related

I'm not sure how to access and compare an object when keys are made using Date.now()

I'm quite new to coding and I'm currently practicing the useReducer() hook in React to manage some state in a simple todo app.
I'm having trouble when trying to implement the TOGGLE_TODO action. I've done it before using arrays, but as I'll likely be working with a lot of objects, I'm trying to figure out why I can't get this right. I'd say I'm learning by failing, but all I'm learning is how to switch the computer off and walk away!
Each time I toggle, I'm passing the state with the spread operator, I've tried it throughout all of the item, I've logged out the key and action.payload to make sure I'm getting a match (it works when I do a simple alert with matching).
I'm aware that the toggle isn't a toggle yet, I was just trying to simply get complete to be true.
I've tried a multitude of things to return state, I've added return to the beginning of the statement, and I"ve encountered some weird bugs along the way. As mentioned, this is quite simple state for now, but it will be more complex in another project I'm working on, so useState get's quite messy.
Any help on what I'm doing wrong here would be highly appreciated.
const initialAppState = {
isOpen: true,
todos: {}
};
export const ACTIONS = {
TOGGLE_MODAL: "toggle-modal",
ADD_TODO: "add-todo",
TOGGLE_TODO: "toggle-todo"
};
const reducer = (state, action) => {
// switch statement for actions
switch (action.type) {
case ACTIONS.TOGGLE_MODAL:
return { ...state, isOpen: !state.isOpen };
case ACTIONS.ADD_TODO:
return {
...state,
todos: {
...state.todos,
// Object is created with Unix code as the key
[Date.now()]: {
todo: action.payload.todo,
complete: false
}
}
};
case ACTIONS.TOGGLE_TODO:
// Comparing the key and the action payload. If they match, it should set complete to 'true'. This will be updated to a toggle when working.
Object.keys(state.todos).map((key) => {
if (key === action.payload) {
return {
...state,
todos: { ...state.todos, [key]: { complete: true } }
};
}
return state;
});
default:
throw new Error("Nope. not working");
}
};
In the render, I pass the key as an id so it can get returned with the payload.
Here is the dispatch function from the component...
const Todo = ({ id, value, dispatch }) => {
return (
<div className="todo">
<h1>{`Todo: ${value.todo}`}</h1>
<p>Done? {`${value.complete}`}</p>
<button
onClick={() =>
dispatch({
type: ACTIONS.TOGGLE_TODO,
payload: id
})
}
>
Mark as Done
</button>
</div>
);
};
and the render is using Object.entries which all works just fine. There were times when I'd get an error, or the initial todo would disappear, so I knew that state wasn't being updated correctly.
Here is the code on CodeSandbox too. I'll update here if I get it working, but I've been stuck here a couple of days. :-(
You were almost there, good idea to index your items with Date.now()!
Only a few issues in the TOGGLE_TODO case:
your reducer should always return a state, your return statement should be at the end of the case, but you put it with the map's function
your reducer should compute a new state, not mutate the current state. So you have to create a new todo object with the complete property.
Here is how it goes:
case ACTIONS.TOGGLE_TODO:
const newTodos = Object.keys(state.todos).map((key) => {
if (key === action.payload) {
return { ...state.todos[key], complete: true } // create a new todo item
}
else {
return state.todos[key]; // keep the existing item
}
});
return {...state, todos: newTodos};

Why we use spread operator into redux

I have seen the following code:
export default function productReducer(state = initialState, action) {
switch(action.type) {
case FETCH_PRODUCTS_BEGIN:
return {
...state,
loading: true,
error: null
};
case FETCH_PRODUCTS_SUCCESS:
return {
...state,
loading: false,
items: action.payload.products
};
case FETCH_PRODUCTS_FAILURE:
return {
...state,
loading: false,
error: action.payload.error,
items: []
};
default:
return state;
}
}
But don't understand why do we use ...state every time for each case.
Can't we just write:
return {
loading: false,
error: action.payload.error,
items: action.payload.products
};
Anyone can explain this?
Because commonly you want to keep other keys inside your state...
If your state has:
{
items:['a', 'b', 'c'],
loading: false,
error: null
}
and you only return for example:
case FETCH_PRODUCTS_BEGIN:
return {
// ...state, --> Without this line
loading: true,
error: null
};
Your new state will be
{
loading: true,
error: null
}
And your items will be lost.
Then, returning:
case FETCH_PRODUCTS_BEGIN:
return {
...state,
loading: true,
error: null
};
You are saying
Return a copy of state, but overriding loading and error keys"
This is for creating new copied state object with new or updated values (without this you would need manually specify every state field).
Object.assign can be used as alternative
Redux Docs has really good explanation about using spread operator.

immutable reducer in redux

Is this reducer looks fine? why the author used a let users;, that look unnecessary to me. Isn't that will cause 2 users in FETCH_USER_FULLFILLED case?
const initalState = {
users: [],
loading: false,
error: null,
};
// REDCUER
function usersReducer(state = initalState, action) {
let users;
switch (action.type) {
case 'FETCH_USER_PENDING':
return { ...state, loading: true };
case 'FETCH_USER_FULFILLED':
users = action.payload.data.results;
return { ...state, loading: false, users };
case 'FETCH_USER_REJECTED':
return { ...state, loading: false, error: `${action.payload.message}` };
default:
return state;
}
}
export default usersReducer;
The reducer is fine and it will not cause two users in FETCH_USER_FULLFILLED. However, you are right, there is no need for let users;. So the code will look like
case 'FETCH_USER_FULFILLED':
return { ...state, loading: false, users: action.payload.data.results };
When the reducer first executed, it will take its start state from initalState as it is the default if no param passed. So users will be empty array at the beginning and will be filled on FETCH_USER_FULLFILLED action
Edit users Added to take advantage of property value shorthand feature in ES6

Reducers for dynamically created items

Is there any particular "good practice" in Redux when it comes to reducing state of dynamically created items? In this particular case, I'm dealing with a list of users that may join/leave the app, tables and games at any time.
let userReducer = (user, action) => {
switch(action.type) {
case 'table:create':
case 'table:join': return {
...user,
tables: [...user.tables, action.tableId]
}
case 'table:leave': return {
...user,
tables: user.tables.filter(tableId => tableId != action.tableId)
};
case 'game:join': return {
...user,
games: [...user.games, action.gameId]
};
case 'game:leave': return {
...user,
games: user.games.filter(gameId => gameId != action.gameId)
};
}
}
let usersById = (users = {}, action) => {
let user = users[action.userId];
switch(action.type) {
case 'user:join': return {
...users,
[action.user.id]: action.user
};
case 'user:leave': {
users = {...users};
delete users[action.userId];
return users;
};
case 'table:create':
case 'table:join':
case 'table:leave':
case 'game:join':
case 'game:leave': return {
...users,
[action.userId]: userReducer(user, action)
};
}
return users;
}
The last five cases in the second function's switch statement look particularly ugly to me. Maybe I could just condense it with an if? (if user is defined, then apply userReducer to it).
let usersById = (users = {}, action) => {
let user = users[action.userId];
if(user)
return {
...users,
[user.id]: userReducer(user, action);
}
switch(action.type) {
case 'user:join': return {
...users,
[action.user.id]: action.user
};
case 'user:leave': {
users = {...users};
delete users[action.userId];
return users;
};
}
return users;
}
I don't think there is any good practice in order to create reducers.
Personally I rather use the approach of your first exemple, as it make your code more readable. In addition it will allow you to keep the same structure to all your reducers.
On the contrary, that looks like some fairly well-organized reducer logic. But yes, if you want to use an if statement like that, you absolutely can - per the Redux FAQ on using switch statements, it's fine to use whatever logic approach you want in a reducer.
For more information on ways to organize reducer logic, see the Redux docs section on "Structuring Reducers", and my recent blog post Idiomatic Redux: The Tao of Redux, Part 2 - Practice and Philosophy

Avoiding repeating state names

Let's say i have a rootreducer like below.
const rootR = combineReducers({
topics,
...
});
the topics reducer
function topics(state = { topics=[], error: null}, action){
switch (action.type){
case types.FETCH_TOPICS_SUCCESSFULLY:
const { topics } = action;
return {
...state,
topics,
};
default:
return state;
}
};
And when i fire the related action i get my state with repeatable properties state.topics.topics instead of state.topics
Is there any way to avoid this repeating (topics.topics)?
Thanks in advance
Looking at the initialState of your topics reducer, the state object accessible to topics reducer has this structure:
{
topics: [],
error: null
}
So when you combineReducers like this:
const rootR = combineReducers({
topics,
anotherReducer,
someOtherReducer.
// ...
});
resulting global app state is going to look like this:
{
topics: {
topics: [],
error: null
},
anotherReducer: {
// ...
},
someOtherReducer: {
// ...
},
// ...
}
So if you want to access topics array from global state, you need to do state.topics.topics.
You have two things under state.topics, an array of topics and error.
Hence let's rename second topics key to items to avoid confusion.
(it is unavoidable to have a second key to store the array because you also want error)
thus we have:
state.topics = {
items: [],
error: null,
}
Instead of state.topics.topics, now we access state.topics.items
To achieve this, initialstate passed to topics reducer has to be:
function topics(state = { items = [], error: null }, action){
//...
}
Now inside the reducer FETCH_TOPICS_SUCCESSFULLY, we want to append an array action.topics to items, like this (without mutating our current state):
case types.FETCH_TOPICS_SUCCESSFULLY:
const { topics } = action;
return {
...state,
items: [
...state.items,
...topics
],
};
#markerikson is right, the state variable passed in the function is actually topics once FETCH_TOPICS_SUCCESSFULLY is called, so it's better to do return topics there.
But given your condition, instead of return {...state, topics} or return topics, you can also do return Object.assign({}, state, topics). This will create a new object with all properties from previous state and topics merged together.
You're double-nesting things. The topics reducer will only see the "topics" slice of state. So, instead of returning {...state, topics}, just do return topics.
update
Your edit to the question changes the situation considerably.
Originally, you had:
function topics(state = {}, action){
Now, you have:
function topics(state = { topics=[], error: null}, action){
I'll admit I'm a bit confused at this point as to what your desired state structure actually should be.
Looking at your original definition, it seemed like you were misunderstanding how combineReducers works, and redundantly trying to return a new object that contained a field/slice named "topics". Now, it looks like the root-level "topics" slice itself has a field named "topics" as well.
Are topics and error supposed to be at the root of your state tree? Or, are they both really supposed to be part of the top-level "topics" slice? If that's really what you want, then you've defined the state tree as needing to be topics.topics.
Also, to answer #free-soul: no, in the original example, return topics would not mutate state, because it's just returning whatever was in the action. Even if the action.topic field was literally the same array that used to be in the state, the result would just be a no-op.

Categories

Resources