Issue with the "U" (update) part of CRUD in reducers - javascript

I've been Working on redux-less CRUD project. Creating a categories list using contextAPI/hooks in combination. I have succeed, the "CR and D" (Create, Read and Delete) parts, but struggling with "U" (update) part, I couldn't get this to work after submit for update change.
Starting with how I did with the reducer components...
reducer.js
import uniqid from 'uniqid';
export const categoryReducer = (state, action) => {
switch (action.type) {
case 'ADD_CATEGORY':
return [
...state,
{
id: uniqid(),
name: action.name,
suffix: action.suffix
}
];
//**MY ISSUES**
case 'UPDATE_CATEGORY':
return (
state.findIndex(item => item.id === action.id),
state.slice(0, action.id, action.name, action.suffix)
);
case 'REMOVE_CATEGORY':
return state.filter(item => item.id !== action.id);
default:
return state;
}
};
the 'ADD_CATEGORY' and 'DELETE_CATEGORY' can be ignored since I don't have issues. The 'UPDATE_CATEGORY' feel a little sketchy to me, I have feeling the syntax is incorrect. From my understanding, in order to make update changes. An selected item need to be scanned from array into matched id. Once the id is matched, The changes can be updated after submit. And I am not sure I could figure out how to add that syntax.
Now the The Edit Form component with dispatch inside handleSubmit function...
EditCategoryForm.js
import React, { useState, useContext, useEffect } from 'react';
//context
import { CategoryContext } from '../contexts/categoryContext';
function EditCategoryForm() {
const { dispatch } = useContext( CategoryContext);
/*Not not get this to work */
const handleSubmit = (e, name, suffix) => {
e.preventDefault();
dispatch({
type: 'EDIT_CATEGORY',
name,
suffix
});
};
const handleChange = e => {
const { name, value } = e.target;
setInputValue(prevState => ({ ...prevState, [name]: value }));
};
return (
<form onSubmit={handleSubmit}>
<h1>Edit</h1>
<input
type="text"
name="name"
value={inputValue.name}
onChange={handleChange}
/>
<input
type="text"
name="suffix"
value={inputValue.suffix}
onChange={handleChange}
/>
<button>Submit Change</button>
<button onClick={() => setEditing(false)}>Cancel</button>
</form>
);
}
export default EditCategoryForm;
Again, I believe, the syntax have been implemented wrong. The goal is to dispatch into separated values of name and suffix based on input value. I didn't not get anything succeed and Its been struggling for me lately. How do I fix this problem? What would be the best practice or a proper way to achieve "U" (update) part inside reducer and dispatcher? Your help is appreciated and thanks in advance.

case 'UPDATE_CATEGORY':
let update_obj = state.find( obj => obj.id === action.id);
update_obj['name'] = action.name;
update_obj['suffix'] = action.suffix;
return [...state, update_obj]
If I am not mistaken, this should return the original state, along with the specific element updated
Or you can do:
const newArr = state.map( obj => {
if obj.id === action.id{
obj['name'] = action.name;
obj['suffix'] = action.suffix;
}
return obj
});
return newArr;

In the update case, I'd recommend mapping through your existing items:
If the mapped item has the same id as the updated one, then replace it
Keep the existing item otherwise
Here's the code:
case 'UPDATE_CATEGORY':
return state.map(item => {
if (item.id === action.id) {
return {...item, name: action.name, suffix: action.suffix };
}
return item;
});
The part where you initialise your state is missing. I'm assuming it's something like this:
const [inputValue, setInputValue] = useState({name: '', suffix: ''})
The handleSubmit function receives a single parameter (event), you need to use the values from your state:
// remove the extra parameters
const handleSubmit = (e /*, name, suffix */) => {
e.preventDefault();
dispatch({
type: 'UPDATE_CATEGORY',
id: inputValue.id,
name: inputValue.name,
suffix: inputValue.suffix,
});
};
Codesandbox here

Related

How to update the state based on my initial state

What I'm trying to do here is to build a dynamic component, that will be responsible to take an array of objects and the form will be built based on my formState. So I made my initial State and used .map to make the loop over the state and mapped the keys to making the label, value, and inputs appear based on the state. but my problem is at onChange. How to update the value key in every object and set the new state for it. any advice, please.
import { useState } from "react";
import InputText from "./components";
import useForm from "./hooks/useForm";
function App() {
interface formStateT {
id: number;
label: string;
value: any;
error: string;
}
const formState = [
{
id: 0,
label: "firstName",
value: "",
error: "",
},
{
id: 1,
label: "lastName",
value: "",
error: "",
},
];
const { form, validate, setForm, checkValidHandler } = useForm(formState);
const [error, setError] = useState("");
const submitFormHandler = (e: { preventDefault: () => void }) => {
e.preventDefault();
checkValidHandler();
// write form logic
// setError() will be used to take the error message
console.log(form);
};
return (
<form onSubmit={(e) => submitFormHandler(e)}>
{form.map((f: formStateT) => (
<InputText
key={f.id}
label={f.label}
value={f.value}
onChange={(e) => {
// Im trying here to update the value key of every label key.
// setForm({ ...form, [f.label.valueOf()]: f.value })
}}
valid={f.value === "" ? validate.notValid : validate.valid}
errorMsg={error === "" ? f.error : error}
classes={"class"}
/>
))}
<button>Submit</button>
</form>
);
}
export default App;
From your comment, f.value = e.target.value; is a state mutation and should be avoided, the setForm([...form]); is masking the mutation.
In App create an onChangeHandler function that takes the onChange event object and the index you want to update. Unpack the value from the onChange event and update the state. The handler should use a functional state update to update from the previous state, and create a shallow copy of the form array, using the index to shallow copy and update the correct array element.
Example:
// curried function to close over index in scope, and
// return handler function to consume event object
const onChangeHandler = index => e => {
const { value } = e.target;
setForm(form => form.map((el, i) =>
i === index
? { ...el, value }
: el
));
};
...
<form onSubmit={submitFormHandler}>
{form.map((f: formStateT, index: number) => (
<InputText
key={f.id}
label={f.label}
value={f.value}
onChange={onChangeHandler(index)} // <-- invoke and pass mapped index
valid={f.value === "" ? validate.notValid : validate.valid}
errorMsg={error === "" ? f.error : error}
classes={"class"}
/>
))}
<button>Submit</button>
</form>

Using args or props to derive data from Redux using selectors

TL;DR: I'm trying to use an id and type parameters in my selectors but the params are undefined. What is the correct way of doing this using reselect and createStructuredSelector?
I'm writing selectors using reselect to get the data needed for a React component. The data is stored in a key/value format where the key is made up of a dynamic id and type value. The object looks like this:
customView: {
'viewBy:deviceProfileId&id:5923f82a-80c2-4c88-bd0e-c105ad989ab2': { // type = 'deviceProfileId' & id = '5923f82a-80c2-4c88-bd0e-c105ad989ab2'
list: { ... },
meta: { ... },
error: { ... }
}
Users enter the id and type and trigger the data fetching from API. I have the basic selector but I'm stuck trying to get the data needed for each entry using these dynamic values - they continue to come out undefined. What am I missing? Is there a better way to accomplish this?
// reducer.js : Fn to generate key
export const customViewKey = ({ customViewType, id } = {}) => {
const typeKey = (customViewType && `viewBy:${customViewType}`) || '';
const idKey = (id && `id:${id}`) || '';
const namespace = `${typeKey}&${idKey}`;
return `${namespace}`;
};
// selector.js
const getCustomView = ({ customView }) => customView; // Basic selector works
export const getCustomViewData = createSelector(
getCustomView,
(customView, id, customViewType) => { // id and customViewType are undefined
return customView && customView[customViewKey({ customViewType, id })];
}
);
// index.js
export const CustomViewsPage = ({ classes, getCustomView, customViewMap, customViewType, id }) => {
const [idValue, setIdValue] = useState('');
const [type, setType] = useState('');
const handleTypeChange = (e) => {
let typeEntered = e ? e.value : '';
setType(typeEntered);
};
const handleInputChange = (e) => {
setIdValue(e.target.value);
};
const handleSubmit = (e) => { // Fn called when user hits submit
e.preventDefault();
getCustomView({ id: idValue, customViewType: type });
};
return (
<div>
<form onSubmit={handleSubmit} className={classes.customViewForm}>
<div>
<p>Select type of custom view:</p>
<Select
placeholder="View messages by"
options={customViewTypes}
onChange={handleTypeChange}
isClearable
/>
</div>
<div>
<p>Enter ID for device, project or device profile:</p>
<InputBase
placeholder="5923f82a-80c2-4c88-bd0e-c105ad989ab2"
required
onChange={handleInputChange}
fullWidth
/>
</div>
<label htmlFor="create-custom-view-btn">
<Input id="create-custom-view-btn" type="submit" />
<Button
component="span"
variant="outlined"
color="primary"
endIcon={<SendRounded />}
onClick={handleSubmit}
>
Create view
</Button>
</label>
</form>
</div>
const mapDispatchToProps = {
getCustomView: requestCustomView,
};
const mapStateToProps = createStructuredSelector({
customViewMap: getCustomViewMap,
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(CustomViewsPage));
I figured out the solution thanks to #markerikson's answer and this blog.
These are the selectors I'm using:
const getCustomView = ({ customView }) => customView;
const getId = (_, id) => id;
const getCustomViewType = (_, id, customViewType) => customViewType;
export const getCustomViewData = createSelector(
getCustomView,
getId,
getCustomViewType,
(customView, id, customViewType) => {
return customView && customView[customViewKey({ customViewType, id })]; // WORKS!
}
);
export const getCustomViewMap = createSelector(getCustomViewData, getDomainMap);
And I'm using useSelector to call the selector from my component this way:
const selectedCustomView = useSelector(state => getCustomViewMap(state, idValue, type));
Yeah, your createSelector call is wrong.
If you want your "output function" to take 3 arguments, then you need to write 3 separate "input functions". Each input function should extract and return a value, and those become the arguments for the output function.
So, you need something like this:
export const getCustomViewData = createSelector(
getCustomView,
// these should also read values from `state` or any other args
getId,
getCustomViewType,
(customView, id, customViewType) => {
// return calculation results here
}
);
See the Redux docs Deriving Data with Selectors usage guide page for more details on how to use Reselect.

React component not updating when redux state changes

I have a React component that maps state to props to get data via redux. Everything works fine with the action and the value being updated properly in the reducer. My only problem is that when the state value changes, I want my component to re render so that it is always displaying the most up to date value in the reducer. As of right now I have to call a separate function that refreshes the component, but I'd rather have it automatically re render every time that value changes in the reducer.
Action:
export const createPickup = (selected, pickups) => dispatch => {
let icon;
icon = check(selected);
pickups.icon = icon;
return API('/createPickUp/', {
...pickups,
})
.then(res => {
dispatch({type: types.CREATE_PICKUP, res});
})
.catch(err => console.log(err));
};
Reducer:
const initialState = {
pick: [],
};
export default function pickup(state = initialState, action) {
switch (action.type) {
case types.GET_PICK:
return {
pick: action.pickup,
};
case types.CREATE_PICKUP:
return {
pick: [action.res, ...state.pick],
};
case types.DEL_GAME:
return {
pick: state.pick.filter(p => p._id !== action.id),
};
case types.START_GAME:
return {
pick: state.pick.map(p =>
p._id === action.id ? {...p, start: true} : p,
),
};
case types.STOP_GAME:
return {
pick: state.pick.map(p =>
p._id === action.id ? {...p, stop: true} : p,
),
};
default:
return state;
}
}
Use useSelector hook in Functional Component as it automatically subscribes to the state and your component will re-render.
If you are using Class Component then use connect() from redux and mapStateinProps.
I am assuming you have passed the reducer to the global Store.
Now... make sure you have the up to date value in your component.. try consoling it like this...
import {useSelector} from 'react-redux';
const YourCmponent = () => {
const reduxState = useSelector(state => state);
console.log(reduxState);
return <div>Your Content</div>
}
That way you can get access to the redux store. And you don't need to make any other function for updating component You will always get updated value here.

React toggle button only works once?

I am learning React Reducer now. I want to build a toggle button that changes a boolean completed value to its opposite each time I click the button.
What I have is an array of states, each state is an object with an id and a completed value set to be true or false. Then I loop through states, setting each state as an Item component and display it on screen.
// App.js file
import React, { useReducer } from "react";
import { AppReducer } from "./AppReducer";
import Item from "./Item";
function App() {
const initialStates = [
{
id: 1,
completed: false,
},
{
id: 2,
completed: false,
},
];
const [states, dispatch] = useReducer(AppReducer, initialStates);
return (
<div>
{states.map((state) => (
<Item item={state} key={state.id} dispatch={dispatch} />
))}
</div>
);
}
export default App;
In the Item component, I display whether this item is completed or not (true or false). I set up a toggle function on the button to change the completed state of the Item.
// Item.js
import React from "react";
const Item = ({ item, dispatch }) => {
function setButtonText(isCompleted) {
return isCompleted ? "True" : "False";
}
let text = setButtonText(item.completed);
function toggle(id){
dispatch({
type: 'toggle',
payload: id
})
text = setButtonText(item.completed);
}
return (
<div>
<button type="button" onClick={() => toggle(item.id)}>Toggle</button>
<span>{text}</span>
</div>
);
};
export default Item;
Here is my reducer function. Basically what I am doing is just loop through the states array and locate the state by id, then set the completed value to its opposite one.
// AppReducer.js
export const AppReducer = (states, action) => {
switch (action.type) {
case "toggle": {
const newStates = states;
for (const state of newStates) {
if (state.id === action.payload) {
const next = !state.completed;
state.completed = next;
break;
}
}
return [...newStates];
}
default:
return states;
}
};
So my problem is that the toggle button only works once. I checked my AppReducer function, it did change completed to its opposite value, however, every time we return [...newStates], it turned back to its previous value. I am not sure why is that. I appreciate it if you can give it a look and help me.
The code is available here.
Here is the working version forked from your codesandbox
https://codesandbox.io/s/toggle-button-forked-jy6jd?file=/src/Item.js
The store value updated successfully. The problem is the way of listening the new item change.
dispatch is a async event, there is no guarantee the updated item will be available right after dispatch()
So the 1st thing to do is to monitor item.completed change:
useEffect(() => {
setText(setButtonText(item.completed));
}, [item.completed]);
The 2nd thing is text = setButtonText(item.completed);, it will not trigger re-render. Therefore, convert the text to state and set it when item.completed to allow latest value to be displayed on screen
const [text, setText] = useState(setButtonText(item.completed));
I have improved your code, just replace your AppReducer code with below.
export const AppReducer = (states, action) => {
switch (action.type) {
case "toggle": {
const updated = states.map((state) =>
action.payload === state.id ? {
...state,
completed: !state.completed
} : { ...state }
);
return [...updated];
}
default:
return states;
}
};
Live demo

React form submission logic with lifted state and controlled dependence

I've dug myself into a deep rabbit hole with this component in an attempt to use React hooks.
The Parent component handles a dictionary state which is eventually distributed to multiple components.
My problem child component WordInput has a form with a single input. When submitting the form the component is fetching the word's definition from an API and passing on both the word and the definition to the parent which then sets the state in the form of dictionary. So far, so good IF it's the first word in dictionary. The part I'm having trouble with is to submit any subsequent words/definitions.
When the user submits a subsequent word, I want the component to check whether the word already exists in the dictionary that is passed to the child. If it doesn't exist, add it to the dictionary via the submit function.
I think the problem is that I'm trying to do too much with useEffect
I useEffect to:
- set loading
- check and process the dictionary for existing words
- check that definition and word aren't empty and submit both to parent/dictionary
- fetch a definition from an API
In the unprocessed code, I have multiple console.groups to help me keep track of what is happening. The more I add to the component, the more Subgroups and subgroups of subgroups accumulate. Clearly, the approach I'm taking isn't very dry and causes too many re-renders of the component/useEffect functions. For conciseness, I have taken out the console.log entries.
The imported fetchWordDefinition merely processes the fetched data and arranges it correctly into an array.
I don't know how to keep this dry and effective, and any help is appreciated with this rather simple task. My hunch is to keep all the logic to submit the word/definition in the submit handler, and only use useEffect to validate the data prior to that.
import React, { useState, useEffect } from "react";
import fetchWordDefinition from "./lib/utils";
const WordInput = ({ onSubmit, dictionary }) => {
const [definition, setDefinition] = useState([]);
const [cause, setCause] = useState({ function: "" });
const [error, setError] = useState({});
const [loading, setLoading] = useState(false);
const [word, setWord] = useState("");
const [wordExistsInDB, setWordExistsInDB] = useState(false);
useEffect(() => {
const dictionaryEmpty = dictionary.length === 0 ? true : false;
if (dictionaryEmpty) {
return;
} else {
for (let i = 0; i < dictionary.length; i += 1) {
if (dictionary[i].word === word) {
setWordExistsInDB(true);
setError({ bool: true, msg: "Word already exists in DB" });
break;
} else {
setWordExistsInDB(false);
setError({ bool: false, msg: "" });
}
}
}
}, [dictionary, word]);
useEffect(() => {
const definitionNotEmpty = definition.length !== 0 ? true : false;
const wordNotEmpty = word !== "" ? true : false;
if (wordNotEmpty && definitionNotEmpty && !wordExistsInDB) {
onSubmit(word, definition);
setWord("");
setDefinition([]);
}
}, [definition, word, onSubmit, wordExistsInDB]);
useEffect(() => {
if (cause.function === "fetch") {
async function fetchFunction() {
const fetch = await fetchWordDefinition(word);
return fetch;
}
fetchFunction().then(definitionArray => {
setDefinition(definitionArray);
setCause({ function: "" });
});
}
}, [cause, word]);
const handleSubmit = async e => {
e.preventDefault();
setLoading(true);
setCause({ function: "fetch" });
};
return (
<form onSubmit={handleSubmit}>
{error.bool ? <span>{error.msg}</span> : null}
<input
name='word'
placeholder='Enter Word'
type='text'
value={word}
onChange={({ target: { value } }) => setWord(value)}
/>
<input type='submit' />
</form>
);
};
export default WordInput;
There are indeed more useEffect's happening than necessary, as well as most of the state. All you need is the handleSubmit to do the fetching.
const WordInput = ({ onSubmit, dictionary }) => {
const [word, setWord] = React.useState("");
const handleChange = React.useCallback(e => {
setWord(e.currentTarget.value)
}, [])
const handleSubmit = React.useCallback(() => {
//check if word is in dictionary
const wordIsAlreadyThere = dictionary.map(entry => entry.word).includes(word)
//fetch the definition, wait for it, and call submit
if(!wordIsAlreadyThere && word.length > 0){
fetchWordDefinition(word)
.then(definition => {
onSubmit(word, definition)
setWord('')
}).catch(err => console.log(err))
}
}, [])
return (
<form onSubmit={handleSubmit}>
<input
value={word}
onChange={handleChange}/>
<input type='submit' />
</form>
);
}
I think you're missing out on some clarity and what useEffect is for
A functional component gets re-ran everytime either a prop or a state changes. useEffect runs when the component gets created, and we use it for things like doing a first-time fetch, or subscribing to an event handler. The second argument (array of variables) is used so that, if we have for example a blog post with with comments etc, we don't re-fetch everything unless the ID changes (meaning it's a new blog post)
Looking at your code, we have this flow:
User inputs something and hits Submit
Check if the word exists in a dictionary
a. If it exists, display an error message
b. If it doesn't exist, fetch from an API and call onSubmit
So really the only state we have here is the word. You can just compute an error based on if the word is in the dictionary, and the API call is done in a callback (useCallback). You have a lot of extra state that doesn't really matter in a state-way
A simplified version would look like this
const WordInput = ({ onSubmit, dictionary }) => {
const [word, setWord] = useState("")
const [loading, setLoading] = useState(false)
// `find` will find the first entry in array that matches
const wordExists = !!dictionary.find(entry => entry.word === word)
// Ternary operator,
const error = (wordExists) ? "Word already exists in DB" : null
// When user hits submit
const handleSubmit = useCallback(() => {
if (wordExists || !word.length) return;
setLoading(true)
fetchFunction()
.then(definitionArray => {
onSubmit(word, definitionArray)
})
}, [])
return (
<form onSubmit={handleSubmit}>
{error && <span>{error}</span>}
<input
name='word'
placeholder='Enter Word'
type='text'
value={word}
onChange={({ target: { value } }) => setWord(value)}
/>
<input type='submit' onclick={handleSubmit} disabled={wordExists}/>
</form>
);
};
Your component only needs to keep track of the word and the loading flag.
When the user changes the word input it updates the word state.
When the user submits the form the loading state changes. This triggers a useEffect that will first check if the word already exists. If not it proceeds to fetch it and add both the word and its definition to the dictionary.
const WordInput = ({ onSubmit, dictionary }) => {
const [loading, setLoading] = useState(false);
const [word, setWord] = useState("");
useEffect(() => {
if (!loading) return;
const existing_word = dictionary.find(item => item.word === word);
if (existing_word) return;
const fetchFunction = async () => {
const definition = await fetchWordDefinition(word);
// Update the dictionary
onSubmit(word, definition);
// Reset the component state
setWord("");
setLoading(false);
};
fetchFunction();
}, [loading]);
return (
<form
onSubmit={e => {
e.preventDefault();
if (word.length) {
setLoading(true);
}
}}
>
<input
name="word"
placeholder="Enter Word"
type="text"
value={word}
onChange={({ target: { value } }) => setWord(value)}
/>
<input type="submit" />
</form>
);
};
Please let me know if something is not clear or I missed something.

Categories

Resources