I'm pretty new to React and React hooks in general,
I'm building a react app for my final project and I wanted to make some component (Advanced search in this example) as generalized as possible which means I want to pass "dataFields" and the component should be updated with a unique state value that originated from those data fields.
I know that I can use a general state and store changes in it with an array but I read that it's bad practice.
this is what I have now:
const [title,updateTitle] = useState({"enable":false,"value": "" });
const [tags,updateTags] = useState({"enable":false,"value": "" });
const [owner,updateOwner] = useState({"enable":false,"value": "" });
const [desc,updateDesc] = useState({"enable":false,"value": "" });
And I try to use this to achieve the same thing:
if(props?.dataFields) {
Object.entries(props.dataFields).forEach ( ([key,value]) => {
// declare state fields
const [key,value] = useState(value)
});
}
what is the proper way of doing it? is there is one?
Do 4 lines of useState or useReducer (local)
I would suggest someting like this for the initial state
const setItem = (enable = false, value = '') => ({ enable, value });
const [title, updateTitle] = useState(setItem());
const [tags, updateTags] = useState(setItem());
const [owner, updateOwner] = useState(setItem());
const [desc, updateDesc] = useState(setItem());
And you also can useReducer and define the initial state.
I add an example for useReducer and case dor change title.value
import React from 'react';
import { useReducer } from 'react';
const setItem = (enable = false, value = '') => ({ enable, value });
const initialState = { title: setItem(), tags: setItem(), owner: setItem(), desc: setItem() };
function reducer(state, action) {
switch (action.type) {
case 'CHANGE_TITLE':
return { ...state, title: setItem(null, action.payload) };
default:
return state;
}
}
function MyFirstUseReducer() {
const [state, dispatch] = useReducer(reducer, initialState);
const updateTitle = ev => {
if (ev.which !== 13 || ev.target.value === '') return;
dispatch({ type: 'CHANGE_TITLE', payload: ev.target.value });
ev.target.value = '';
};
return (
<>
<h2>Using Reducer</h2>
<input type="text" onKeyUp={updateTitle} placeholder="Change Title" />
<div>
<span>The State Title is: <strong>{state.title.value}</strong></span>
</div>
</>
);
}
export default MyFirstUseReducer;
Related
The object of this app is to allow input text and URLs to be saved to localStorage. It is working properly, however, there is a lot of repeat code.
For example, localStoredValues and URLStoredVAlues both getItem from localStorage. localStoredValues gets previous input values from localStorage whereas URLStoredVAlues gets previous URLs from localStorage.
updateLocalArray and updateURLArray use spread operator to iterate of previous values and store new values.
I would like to make the code more "DRY" and wanted suggestions.
/*global chrome*/
import {useState} from 'react';
import List from './components/List'
import { SaveBtn, DeleteBtn, DisplayBtn, TabBtn} from "./components/Buttons"
function App() {
const [myLeads, setMyLeads] = useState([]);
const [leadValue, setLeadValue] = useState({
inputVal: "",
});
//these items are used for the state of localStorage
const [display, setDisplay] = useState(false);
const localStoredValues = JSON.parse(
localStorage.getItem("localValue") || "[]"
)
let updateLocalArray = [...localStoredValues, leadValue.inputVal]
//this item is used for the state of localStorage for URLS
const URLStoredVAlues = JSON.parse(localStorage.getItem("URLValue") || "[]")
const tabBtn = () => {
chrome.tabs.query({ active: true, currentWindow: true }, function (tabs) {
const url = tabs[0].url;
setMyLeads((prev) => [...prev, url]);
// update state of localStorage
let updateURLArray = [...URLStoredVAlues, url];
localStorage.setItem("URLValue", JSON.stringify(updateURLArray));
});
setDisplay(false)
};
//handles change of input value
const handleChange = (event) => {
const { name, value } = event.target;
setLeadValue((prev) => {
return {
...prev,
[name]: value,
};
});
};
const saveBtn = () => {
setMyLeads((prev) => [...prev, leadValue.inputVal]);
setDisplay(false);
// update state of localStorage
localStorage.setItem("localValue", JSON.stringify(updateLocalArray))
};
const displayBtn = () => {
setDisplay(true);
};
const deleteBtn = () => {
window.localStorage.clear();
setMyLeads([]);
};
const listItem = myLeads.map((led) => {
return <List key={led} val={led} />;
});
//interates through localStorage items returns each as undordered list item
const displayLocalItems = localStoredValues.map((item) => {
return <List key={item} val={item} />;
});
const displayTabUrls = URLStoredVAlues.map((url) => {
return <List key={url} val={url} />;
});
return (
<main>
<input
name="inputVal"
value={leadValue.inputVal}
type="text"
onChange={handleChange}
required
/>
<SaveBtn saveBtn={saveBtn} />
<TabBtn tabBtn={tabBtn} />
<DisplayBtn displayBtn={displayBtn} />
<DeleteBtn deleteBtn={deleteBtn} />
<ul>{listItem}</ul>
{/* displays === true show localstorage items in unordered list
else hide localstorage items */}
{display && (
<ul>
{displayLocalItems}
{displayTabUrls}
</ul>
)}
</main>
);
}
export default App
Those keys could be declared as const and reused, instead of passing strings around:
const LOCAL_VALUE = "localValue";
const URL_VALUE = "URLValue";
You could create a utility function that retrieves from local storage, returns the default array if missing, and parses the JSON:
function getLocalValue(key) {
return JSON.parse(localStorage.getItem(key) || "[]")
};
And then would use it instead of repeating the logic when retrieving "localValue" and "URLValue":
const localStoredValues = getLocalValue(LOCAL_VALUE)
//this item is used for the state of localStorage for URLS
const URLStoredVAlues = getLocalValue(URL_VALUE)
Similarly, with the setter logic:
function setLocalValue(key, value) {
localStorage.setItem(key, JSON.stringify(value))
}
and then use it:
// update state of localStorage
let updateURLArray = [...URLStoredVAlues, url];
setLocalValue(URL_VALUE, updateURLArray);
// update state of localStorage
setLocalValue(LOCAL_VALUE, updateLocalArray)
I'm currently working on a poll app which is supposed to sequentially render a list of questions and post answers to the server. I have no problem handling answers but looping through questions gave me some trouble.
Here is a flow of my code:
PollContainer.js - component
import React, { useState, useEffect } from 'react'
import { useSelector, useDispatch } from 'react-redux'
import Question from './Questions/Question';
import { pushAnswers } from '../../actions/answers';
import { incrementCounter } from '../../actions/utils';
import Thanks from './Thanks'
const PollContainer = () => {
const dispatch = useDispatch();
const questions = useSelector(state => state.questions); // an array of questions
// a counter redux state which should increment at every click of 'submit' inside a question
const utils = useSelector(state => state.utils);
let activeQuestion = questions[utils.counter];
// function passed as a prop to a singular Question component to handle submit of an answer
const pushSingleAnswer = (answer) => {
let answersUpdate = state.answers;
answersUpdate.push(answer);
console.log(`counter:${utils.counter}`) // logs 0 everytime I click submit, while redux dev tools show that utils.counter increments at every click
if (utils.counter < questions.length ) {
setState({...state, answers: answersUpdate, activeQuestion: activeQuestion});
dispatch(incrementCounter());
} else{
dispatch(pushAnswers(state.answers));
setState({...state, isFinished: true});
}
};
const [state, setState] = useState({
isFinished: false,
activeQuestion: questions[0],
answers: [],
pushSingleAnswer
})
return (
(utils.isLoading) ? (
<h1>Loading questions...</h1>
) : (
<div>
{(!state.isFinished && <Question { ...state }/>)}
{(state.isFinished && <Thanks/>)}
</div>
))
}
export default PollContainer;
incrementCounter action:
import * as types from "./types";
export const incrementCounter = () => {
return {
type: types.INCREMENT_COUNTER,
}
}
utils.js - reducer
// reducer handles what to do on the news report (action)
import * as types from '../actions/types';
const initialState = {
isLoading: false,
error: null,
counter: 0
}
export default (utils = initialState, action) => {
switch(action.type){
case types.LOADING_DATA:
return {...utils, isLoading: true};
case types.DATA_LOADED:
return {...utils, isLoading: false};
case types.ACTION_FAILED:
return {...utils, error: action.error};
case types.INCREMENT_COUNTER:
return {...utils, counter: utils.counter + 1} // here is the incrementing part
default:
return utils;
}
}
utils.counter that is passed to pushSingleAnswer function doesn't increment, however redux dev tools tells me it does increase every time I click submit in a Question component. Because of that it doesn't render next questions. The submit handler in Question component is simply this:
const handleSubmit = (e) => {
e.preventDefault();
props.pushSingleAnswer(state);
};
I also tried with:
useEffect(() => {
dispatch(incrementCounter())},
[state.answers]
);
expecting it'll increment every time there's an update to state.answers but it doesn't work either. Morover the counter in redux-dev-tools doesn't increment either.
I'd be very grateful for any suggestions, this is my first serious react-redux project and I really enjoy working with these technologies. However I do not quite understand how react decides to render stuff on change of state.
Issue
You are closing over the initial counter state in the pushSingleAnswer callback stored in state and passed to Question component.
You are mutating your state object in the handler.
Code:
const pushSingleAnswer = (answer) => {
let answersUpdate = state.answers; // <-- save state reference
answersUpdate.push(answer); // <-- state mutation
console.log(`counter:${utils.counter}`) // <-- initial value closed in scope
if (utils.counter < questions.length ) {
setState({
...state, // <-- persists closed over callback/counter value
answers: answersUpdate,
activeQuestion: activeQuestion,
});
dispatch(incrementCounter());
} else{
dispatch(pushAnswers(state.answers));
setState({ ...state, isFinished: true });
}
};
const [state, setState] = useState({
isFinished: false,
activeQuestion: questions[0],
answers: [],
pushSingleAnswer // <-- closed over in initial state
});
{(!state.isFinished && <Question { ...state }/>)} // <-- stale state passed
Solution
Don't store the callback in state and use functional state updates.
const pushSingleAnswer = (answer) => {
console.log(`counter:${utils.counter}`) // <-- value from current render cycle
if (utils.counter < questions.length ) {
setState(prevState => ({
...prevState, // <-- copy previous state
answers: [
...prevState.answers, // <-- copy previous answers array
answer // <-- add new answer
],
activeQuestion,
}));
dispatch(incrementCounter());
} else{
dispatch(pushAnswers(state.answers));
setState({ ...state, isFinished: true });
}
};
const [state, setState] = useState({
isFinished: false,
activeQuestion: questions[0],
answers: [],
});
{!state.isFinished && (
<Question { ...state } pushSingleAnswer={pushSingleAnswer} />
)}
so I am making a budget tracking app where the user can add their income sources to an incomes list and expenses to an expenses list, and I got it working, but I wanted to see if I could use useReducer instead of using useState so many times. This is where I am stuck since I am not sure what to return in the reducer.
I am using 2 state objects, incomes and expenses. Basically for now I want to use a reducer to allow the user to add an income source to the incomes object. I want to see if I could set the incomes object inside the reducer, and when dispatch is called with the action set to ADD_INCOME_ITEM, budgetObj.type will be set to + and setIncomes(incomes.concat(budgetObj)) will be called (the income source will be added to the incomes list). I hope I made this clear!
App.js:
import React, { useState, useReducer } from 'react';
import './App.css';
import BudgetInput from './components/input/BudgetInput';
import BudgetOutput from './components/output/BudgetOutput';
import IncomeOutputList from './components/output/IncomeOutputList';
import ExpenseOutputList from './components/output/ExpenseOutputList';
// custom hook
const useSemiPersistentState = (key, initialState) => {
const [value, setValue] = React.useState(
localStorage.getItem(key) ? JSON.parse(localStorage.getItem(key)) : initialState
);
React.useEffect(()=>{
localStorage.setItem(key, JSON.stringify(value));
}, [value, key])
return [value, setValue];
};
const App = () => {
// want to replace these 5 lines with useReducer
const [incomes, setIncomes] = useSemiPersistentState('income',[{}]);
const [expenses, setExpenses] = useSemiPersistentState('expense',[{}]);
const [description, setDescription] = useState('');
const [type, setType] = useState('+');
const [value, setValue] = useState('');
const budgetObj = {
desc: description,
budgetType: type,
incomeValue: value
}
const initialbudget = {
desc: '',
budgetType: '+',
incomeValue: ''
}
const budgetReducer = (state, action) => {
switch(action.type) {
case 'ADD_INCOME_ITEM': //want to set the incomes object here
return setIncomes(incomes.concat(budgetObj)); // not sure if this is correct??
// also set state here???
}
//will add more cases here
}
const [budget, dispatchBudget] = useReducer( //reducer, initial state
budgetReducer,
initialbudget
);
const handleBudgetObjArray = () => {
if(budgetObj.budgetType === '+') {
setIncomes(incomes.concat(budgetObj)); //want to move this to reducer
}
else if(budgetObj.budgetType === '-') {
setExpenses(expenses.concat(budgetObj)); //want to move this to reducer
}
}
const handleChange = (event) => {
setDescription(event.target.value);
}
const handleSelectChange = (event) => {
setType(event.target.value);
}
const handleValueChange = (event) => {
setValue(event.target.value);
console.log(budgetObj)
}
const removeInc = (index) => {
let items = JSON.parse(localStorage.getItem("income"));
items.splice(index, 1);
setIncomes(items);
}
const removeExp = (index) => {
let items = JSON.parse(localStorage.getItem("expense"));
items.splice(index, 1);
setExpenses(items);
}
return (
<div className="App">
<link href="http://code.ionicframework.com/ionicons/2.0.1/css/ionicons.min.css" rel="stylesheet" type="text/css"></link>
<div className="top">
<BudgetOutput />
</div>
<div className="bottom">
<BudgetInput
descValue={description}
onDescChange={handleChange}
onSelectChange={handleSelectChange}
type={type}
onBudgetSubmit={handleBudgetObjArray}
budgetValue={value}
onValChange={handleValueChange}
/>
<div className="container clearfix">
<IncomeOutputList
list={incomes}
removeIncome={(index)=>removeInc(index)}
/>
<ExpenseOutputList
list={expenses}
removeExpense={(index)=>removeExp(index)}
/>
</div>
</div>
</div>
)
};
export default App;
This file is where budgetObj is set:
import React from 'react';
import IncomeOutput from './IncomeOutput';
// list will be list of income objects
const IncomeOutputList = ({ list, removeIncome }) => {
return (
<div className="income__list">
<div className="income__list--title">INCOME</div>
{list.map((item, index, arr) => <IncomeOutput
id={item.id}
value={item.incomeValue}
type={item.budgetType}
desc={item.desc}
// handleButton={handler(index)}
handleButton={()=>removeIncome(index)}
/>
)}
</div>
)
}
export default IncomeOutputList;
The useReducer replaces useState. It is your state. So this right here makes no sense.
case 'ADD_INCOME_ITEM': //want to set the incomes object here
return setIncomes(incomes.concat(budgetObj)); // not sure if this is correct??
Those five useState lines of your code which include incomes and setIncomes are going to be totally deleted, so you cannot be using them in your reducer.
It looks like the initialState for your reducer is just one budget object. It needs to be an object that represents the entire component state. Something like this:
const initialBudget = {
description: '',
type: '+',
value: '',
};
const initialState = {
incomes: [{}],
expenses: [{}],
budgetObj: initialBudget,
};
I am defining the initialBudget separately so that we can use it to reset the budgetObj easily.
Your reducer handles actions by taking the state and the action and returning the next state, like this:
const budgetReducer = (state, action) => {
switch(action.type) {
case 'SUBMIT_BUDGET':
// I am using spread to clone the object to be safe, might not be 100% neccessary
const budget = {...state.budget};
// figure out where to add the current budget object
const isIncome = budget.budgetType === '+';
return {
...state, // not actually necessary in this case since we are updating every property
incomes: isIncome ? state.incomes.concat(budget) : state.incomes, // maybe add to incomes
expenses: isIncome ? state.expenses : state.expenses.concat(budget), // maybe add to expenses
budgetObj: initialBudget, // reset budget object
}
default:
return state;
}
}
I am dynamically adding <div> elements to a component by adding them to an array. This is not a problem and works well. The issue I'm trying to solve here is removing the <div> on double click by passing the id of the <div> that was doubled clicked with props when the reducer is dispatched.
The main issue is the array filter function only works when I code hard the div id both on the div and in the filter function when I want to pass the id of e.target.id on dispatch of delDiv reducer.
Note: I can remove the div successfully by changing the addDivReducer like this:
case "ADD_DIV":
return state.concat(
<DivComponent
key={Math.floor(Math.random() * 100) + 1}
id={11} ***************************************************** Changed
/>
);
case "DELETE_DIV":
state = state.filter((elements) => {
return elements.props.id !== 11; *********************************** Changed
});
return state;
But the desired effect is to pass id as props on dispatch as seen in my code below
The reducer that adds a removes elements look like this:
import DivComponent from "../../components/AddDivComponent";
const addDivReducer = (state, action) => {
switch (action.type) {
case "ADD_DIV":
return state.concat(
<DivComponent
key={Math.floor(Math.random() * 100) + 1}
id={Math.floor(Math.random() * 100) + 1}
/>
);
case "DELETE_DIV":
state = state.filter((elements) => {
return elements.props.id !== action.payload;
});
return state;
default:
return (state = []);
}
};
export default addClipartReducer;
The actions index.js look like:
export const addDiv = (props) => {
return {
type: "ADD_DIV",
payload: props,
};
};
export const deleteDiv = (props) => {
return {
type: "DELETE_DIV",
payload: props,
};
};
The delete reducer is being dispatched when the div is double clicked on like this in AddDivComponent.js:
import { useDispatch } from "react-redux";
import { deleteDiv } from "../../store/actions";
const AddDivComponent = (props) => {
const dispatch = useDispatch();
const removeClipart = (e) => {
dispatch(deleteDiv(e.target.id));
};
return(
<div
id={props.id}
className="my-div"
onDoubleClick={removeDiv}
/>
);
};
export default DivComponent;
Finally the array of <div> elements is being shown here in Canvas.js:
import { useSelector } from "react-redux";
const Canvas = () => {
const divList = useSelector((state) => state.addDIV);
return(
<div className="canvas">
{divList}
</div>
);
};
export default Canvas;
you are mutating state at your DELETE_DIV reducer. If you need to handle state, create a copy a first:
// mutating state here to a new value, can lead to problems
state = state.filter((elements) => {
return elements.props.id !== action.payload;
});
I would suggest to return filter directly, given filter already returns the desired next state, while not mutating the original:
case "DELETE_DIV":
return state.filter((elements) => {
return elements.props.id !== action.payload;
});
I am attempting to create a hook which allows a component to subscribe to a part of the global state changing. For example, imagine my state looks like this
{
products: []
userForm: {
name: 'John Smith',
dateOfBirth: '07/10/1991'
}
}
The component which controls the dateOfBirth field in the userForm should only re-render if the dateOfBirth field changes.
Say I have some global state created using React context. Here is my attempt at subscribing to the field of the global state that that component cares about
function useField(field) {
const [globalState, setGlobalState] = useContext(GlobalState);
const value = globalState[field] || "initial";
const setValue = useCallback(
(value) => {
setGlobalState((state) => ({
...state,
[field]: value
}));
},
[setGlobalState, field]
);
return [value, setValue];
}
Demo https://codesandbox.io/s/dawn-fog-ieqxs?file=/src/App.js:326-612
The above code causes any component which uses the useField hook to rerender.
The desired behaviour is that the component should only rerender when that field changes.
It can work, but not with Context API, as for now Context API can't bailout of useless renders.
In other words: components subscribed to context provider will always render on provider value change.
An example of Context API known problem:
const GlobalContext = React.createContext(null);
const InnerComponent = () => {
/* eslint-disable no-unused-vars */
const { uselessState } = useContext(GlobalContext);
console.log(`Inner rendered`);
return <></>;
};
const InnerMemo = React.memo(InnerComponent);
const InnerComponentUsingContext = () => {
const { counter, dispatch } = useContext(GlobalContext);
console.log(`Inner Using Context rendered`);
return (
<>
<div>{counter}</div>
<button onClick={() => dispatch()}>Dispatch</button>
</>
);
};
const InnerComponentUsingContextMemo = React.memo(InnerComponentUsingContext);
const App = () => {
const [counter, dispatch] = useReducer((p) => p + 1, 0);
const [uselessState] = useState(null);
return (
<GlobalContext.Provider value={{ counter, uselessState, dispatch }}>
<InnerMemo />
<InnerComponentUsingContextMemo />
</GlobalContext.Provider>
);
};
That said, using every modern state management solution has a bailout function which will resolve this issue:
// Always renders
const [globalState, setGlobalState] = useContext(GlobalState);
const value = globalState[field] || "initial";
// Bailout, for example with redux
const value = useReducer(globalState => globalState[field], /* Can add bailout function here if necessary */);