How to combine my React state and another boolean - reactjs

I have a banner in my react app which I can close:
I persist this state in localStorage:
const [bannerShown, setBannerShown] = useState(true);
useEffect(() => {
const data = localStorage.getItem('MY_APP_STATE');
if (data !== null) {
setBannerShown(JSON.parse(data));
}
}, []);
useEffect(() => {
localStorage.setItem('MY_APP_STATE', JSON.stringify(bannerShown));
}, [bannerShown]);
{bannerShown && (<MyBanner onClick={() => setBannerShown(false)} />)}
This is working fine. Now I want to add a condition:
I only want to show the banner when it contains a certain query param:
import queryString from 'query-string';
const queryParams = queryString.parse(location.search);
const hasQueryParam = queryString
.stringify(queryParams)
.includes('foo=bar');
How do I combine above state and boolean (hasQueryParam) in a clean way?

You can chain together several statements in the JSX {bannerShown && hasQueryParam && (<MyBanner onClick={() => setBannerShown(false)} />)}
Or you could put this into an if statement
let showItem;
if (bannerShown && hasQueryParam) {
showItem = <MyBanner onClick={() => setBannerShown(false)} />
}
Then in the JSX {showItem}

set state true if query-string has any value
useEffect(() => {
const foo = qs.parse(location.search);
console.log(foo)
if(!Object.keys(foo).length === 0){
setBanner(true)
}else{
setBanner(false)
}
}, [])

Related

React | Collect State Values of Children Array and update Per Object and Save to PouchDB

Stackoverflow
problem
I have separate components that house Tiptap Editor tables. At first I had a save button for each Child Component which worked fine, but was not user friendly. I want to have a unified save button that will iterate through each child Table component and funnel all their editor.getJSON() data into an array of sections for the single doc object . Then finish it off by saving the whole object to PouchDB
What did I try?
link to the repo → wchorski/Next-Planner: a CRM for planning events built on NextJS (github.com)
Try #1
I tried to use the useRef hook and the useImperativeHandle to call and return the editor.getJSON(). But working with an Array Ref went over my head. I'll post some code of what I was going for
// Parent.jsx
const childrenRef = useRef([]);
childrenRef.current = []
const handleRef = (el) => {
if(el && !childrenRef.current.includes(el)){
childrenRef.current.push(el)
}
}
useEffect(() =>{
childrenRef.current[0].childFunction1() // I know this doesn't work, because this is where I gave up
})
// Child.jsx
useImperativeHandle(ref, () => ({
childFunction1() {
console.log('child function 1 called');
},
childFunction2() {
console.log('child function 2 called');
},
}))
Try #2
I set a state counter and passed it down as a prop to the Child Component . Then I update the counter to trigger a child function
// Parent.jsx
export const Planner = ({id, doc, rev, getById, handleSave, db, alive, error}) => {
const [saveCount, setSaveCount] = useState(0)
const handleUpdate = () =>{
setSaveCount(prev => prev + 1)
}
const isSections = () => {
if(sectionsState[0]) handleSave(sectionsState)
if(sectionsState[0] === undefined) console.log('sec 0 is undefined', sectionsState)
}
function updateSections(newSec) {
setsectionsState(prev => {
const newState = sectionsState.map(obj => {
if(!obj) return
if (obj.header === newSec.header) {
return {...obj, ...newSec}
}
// 👇️ otherwise return object as is
return obj;
});
console.log('newState', newState);
return newState;
});
}
useEffect(() => {
setsectionsState(doc.sections)
}, [doc])
return (<>
<button
title='save'
className='save'
onPointerUp={handleUpdate}>
Save to State <FiSave />
</button>
<button
style={{right: "0", width: 'auto'}}
title='save'
className='save'
onClick={isSections}>
Save to DB <FiSave />
</button>
{doc.sections.map((sec, i) => {
if(!sec) return
return (
<TiptapTable
key={i}
id={id}
rev={doc.rev}
getById={getById}
updateSections={updateSections}
saveCount={saveCount}
section={sec}
db={db}
alive={alive}
error={error}
/>
)
})}
</>)
// Child.jsx
export const TiptapTable = ((props, ref) => {
const {id, section, updateSections, saveCount} = props
const [currTimeStart, setTimeStart] = useState()
const [defTemplate, setdefTemplate] = useState('<p>loading<p>')
const [isLoaded, setIsLoaded] = useState(false)
const [notesState, setnotesState] = useState('')
const editor = useEditor({
extensions: [
History,
Document,
Paragraph,
Text,
Gapcursor,
Table.configure({
resizable: true,
}),
TableRow.extend({
content: '(tableCell | tableHeader)*',
}),
TableHeader,
TableCell,
],
// i wish it was this easy
content: (section.data) ? section.data : defTemplate,
}, [])
const pickTemplate = async (name) => {
try{
const res = await fetch(`/templates/${name}.json`,{
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
const data = await res.json()
setIsLoaded(true)
setdefTemplate(data)
console.log('defTemplate, ', defTemplate);
// return data
} catch (err){
console.warn('template error: ', err);
}
}
function saveData(){
console.log(' **** SAVE MEEEE ', section.header);
try{
const newSection = {
header: section.header,
timeStart: currTimeStart,
notes: notesState,
data: editor.getJSON(),
}
updateSections(newSection)
} catch (err){
console.warn('table update error: ', id, err);
}
}
useEffect(() => {
// 👇️ don't run on initial render
if (saveCount !== 0) saveData()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [saveCount])
useEffect(() => {
setTimeStart(section.timeStart)
setnotesState(section.notes)
if(!section.data) pickTemplate(section.header).catch(console.warn)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [id, section, isLoaded])
useEffect(() => {
if (editor && !editor.isDestroyed) {
if(section.data) editor.chain().focus().setContent(section.data).run()
if(!section.data) editor.chain().focus().setContent(defTemplate).run()
setIsLoaded(true)
}
}, [section, defTemplate, editor]);
if (!editor) {
return null
}
return isLoaded ? (<>
<StyledTableEditor>
<div className="title">
<input type="time" label='Start Time' className='time'
onChange={(e) => setTimeStart(e.target.value)}
defaultValue={currTimeStart}
/>
<h2>{section.header}</h2>
</div>
<EditorContent editor={editor} className="tiptap-table" ></EditorContent>
// ... non relavent editor controls
<button
title='save'
className='save2'
onPointerUp={() => saveData()}>
Save <FiSave />
</button>
</div>
</nav>
</StyledTableEditor>
</>)
: null
})
TiptapTable.displayName = 'MyTiptapTable';
What I Expected
What I expected was the parent state to update in place, but instead it overwrites the previous tables. Also, once it writes to PouchDB it doesn't write a single piece of new data, just resolved back to the previous, yet with an updated _rev revision number.
In theory I think i'd prefer the useRef hook with useImperativeHandle to pass up the data from child to parent.
It looks like this question is similar but doesn't programmatically comb through the children
I realize I could have asked a more refined question, but instead of starting a new question I'll just answer my own question from what I've learned.
The problem being
I wasn't utilizing React's setState hook as I iterated and updated the main Doc Object
Thanks to this article for helping me through this problem.
// Parent.jsx
import React, {useState} from 'react'
import { Child } from '../components/Child'
export const Parent = () => {
const masterDoc = {
_id: "123",
date: "2023-12-1",
sections: [
{header: 'green', status: 'old'},
{header: 'cyan', status: 'old'},
{header: 'purple', status: 'old'},
]
}
const [saveCount, setSaveCount] = useState(0)
const [sectionsState, setsectionsState] = useState(masterDoc.sections)
function updateSections(inputObj) {
setsectionsState(prev => {
const newState = prev.map(obj => {
// 👇️ if id equals 2, update country property
if (obj.header === inputObj.header)
return {...obj, ...inputObj}
return obj;
});
return newState;
});
}
return (<>
<h1>Parent</h1>
{sectionsState.map((sec, i) => {
if(!sec) return
return (
<Child
key={i}
section={sec}
updateSections={updateSections}
saveCount={saveCount}
/>
)
})}
<button
onClick={() => setSaveCount(prev => prev + 1)}
>State dependant update {saveCount}</button>
</>)
}
// Child.jsx
import React, {useEffect, useState, forwardRef, useImperativeHandle} from 'react'
export const Child = forwardRef((props, ref) => {
const {section, updateSections, saveCount} = props
const [statusState, setStatusState] = useState(section.status)
function modData() {
const obj = {
header: section.header,
status: statusState
}
updateSections(obj)
}
useEffect(() => {
// 👇️ don't run on initial render
if (saveCount !== 0) modData()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [saveCount])
return (<>
<span style={{color: section.header}}>
header: {section.header}
</span>
<span>status: {section.status}</span>
<input
defaultValue={section.status}
onChange={(e) => setStatusState(e.target.value)}
/>
________________________________________
</>)
})
Child.displayName = 'MyChild';

React Hooks: How to get updated value of a state in useEffect

Need to update the boolean state value on map click. And based on that updated value, new component rendered.
const [layerClick, setLayerClick] = useState(false);
useEffect(() => {
if (streetsCurrentHeatStateMap !== null) {
streetsCurrentHeatStateMap.on('click', (e) => {
if (layerClick === false) {
popupToggler();
}
})
}
}, [
layerClick,
]);
And here is the popupToggler function
const popupToggler = async () => {
setLayerClick((layerClick) => !layerClick);
}
And it is used for the conditional rendering of the Popup Component.
{layerClick && (
<PopupContent />
)}
So problem is: It always call the popuptoggler function, mean the value of state not updated. Any help?

React How to use query string when using react-router-dom v6

I am currently building an e-commerce website with React, and I have question about query params.
In every e-commerce website, there are filters. I want to use useSearchParams for this filter (sort, conditions, min price, max price, categories, etc.). But I am not 100% sure if I am using searchParams correctly.
I am currently using react state for conditions and sort, and update searchParams using useEffect.
Code:
export default function Shop() {
const [searchParams, setSearchParams] = useSearchParams();
const [sort, setSort] = useState("newest");
const [conditions, setConditions] = useState([]);
const handleChangeSort = (e) => {
setSort(e.target.value);
searchParams.set("sort", sort);
setSearchParams(searchParams);
};
const handleChangeConditions = (e, checked, newValue) => {
if (checked) {
const newList = [...conditions, newValue];
setConditions(newList);
return;
}
const newList = conditions.filter((condition) => condition !== newValue);
setConditions(newList);
};
const handleReset = () => {
setSort("newest");
setConditions([]);
};
useEffect(() => {
if (conditions.length === 0) {
searchParams.delete("conditions");
setSearchParams(searchParams);
return;
}
searchParams.set("conditions", JSON.stringify(conditions));
setSearchParams(searchParams);
}, [conditions, searchParams, setSearchParams]);
return (
<>
<Sort sort={sort} onChange={handleChangeSort} />
<Conditions conditions={conditions} onChange={handleChangeConditions} />
<Button variant="contained" onClick={handleReset}>
Reset Filter
</Button>
</>
);
}
Link to CodeSandbox
Is this right approach? Or am I doing something wrong?
The "right" approach is the one that works for your use case. I don't see any overt issues with the way you are using the queryString parameters. Here I'd say if you want the React state to be the source of truth and the queryString as the cached version then you'll want to initialize the state from the query params and persist the state updates to the query params. This keeps the query params and local component state synchronized.
Example:
export default function Shop() {
const [searchParams, setSearchParams] = useSearchParams();
// Initialize states from query params, provide fallback initial values
const [sort, setSort] = useState(searchParams.get("sort") || "newest");
const [conditions, setConditions] = useState(
JSON.parse(searchParams.get("conditions")) || []
);
// Handle updating sort state
const handleChangeSort = (e) => {
setSort(e.target.value);
};
// Persist sort to query params
useEffect(() => {
searchParams.set("sort", sort);
setSearchParams(searchParams);
}, [searchParams, setSearchParams, sort]);
// Handle updating conditions state
const handleChangeConditions = (e, checked, newValue) => {
if (checked) {
setConditions((conditions) => conditions.concat(newValue));
} else {
setConditions((conditions) =>
conditions.filter((condition) => condition !== newValue)
);
}
};
// Persist conditions to query params
useEffect(() => {
if (conditions.length) {
searchParams.set("conditions", JSON.stringify(conditions));
} else {
searchParams.delete("conditions");
}
setSearchParams(searchParams);
}, [conditions, searchParams, setSearchParams]);
const handleReset = () => {
setSort("newest");
setConditions([]);
};
return (
<>
<Sort sort={sort} onChange={handleChangeSort} />
<Conditions conditions={conditions} onChange={handleChangeConditions} />
<Button variant="contained" onClick={handleReset}>
Reset Filter
</Button>
</>
);
}

Why is my Saved State Overwritten on Refresh in this Simple React Example?

I am following this simple tutorial and can't get my saved state to work.
I can see in the comments that other users are having to work around this issue.
import React, { useState, useRef, useEffect } from "react"
import TodoList from "./TodoList"
import { v4 as uuidv4 } from 'uuid';
const LOCAL_STORAGE_KEY = 'todosApp.todos'
function App() {
const [todos, setTodos] = useState([])
const todoNameRef = useRef()
useEffect(() => {
console.log(`useEffect[]`)
const storedTodos = JSON.parse(localStorage.getItem(LOCAL_STORAGE_KEY));
if (storedTodos) {
console.log(`set todos to: ${JSON.stringify(storedTodos)}`)
setTodos(storedTodos)
// can't print here - value is set asynchronously
// console.log(`loaded todos: ${JSON.stringify(todos)}`)
}
}, [])
useEffect(() => {
console.log(`useEffect[todos]: ${JSON.stringify(todos)}`)
if (todos.length != 0) {
console.log('save')
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(todos))
}
}, [todos])
function toggleTodo(id) {
const newTodos = [...todos]
const todo = newTodos.find(todo => todo.id === id)
console.log(`toggleTodo: ${todo.name}`)
todo.complete = !todo.complete
setTodos(newTodos)
}
function handleAddTodo(e) {
const name = todoNameRef.current.value
console.log(`handleAddTodo: ${name}`)
// setTodos(todos.concat({completed: false, name: todoNameRef.current.value}))
if (name === '') {
return
}
setTodos([...todos, { id:uuidv4(), name:name, complete:false }])
}
return (
<div>
<TodoList todos={todos} handleCheckboxChanged={toggleTodo}/>
<input ref={todoNameRef} type="text" />
<button onClick={handleAddTodo}>Add Todo</button>
<button>Clear Completed</button>
<div>0 left to do</div>
</div>
)
}
export default App;
Here's the output:
useEffect[]
App.js:15 set todos to: [{"id":"77fe1e9e-91aa-4a34-9bfb-b1842ea5518d","name":"asfd","complete":false},{"id":"8dabea66-4ed9-4f10-9003-af1b34b4558a","name":"asfd","complete":false},{"id":"6d4e9350-11cd-4ace-8766-485e1f8817ad","name":"asfd","complete":false}]
App.js:23 useEffect[todos]: []
App.js:12 useEffect[]
App.js:15 set todos to: []
App.js:23 useEffect[todos]: []
App.js:23 useEffect[todos]: []
So it seems like the state is asynchronously initialised after loading the state for some reason.
Here is my workaround:
useEffect(() => {
console.log(`useEffect[todos]: ${JSON.stringify(todos)}`)
if (todos.length != 0) {
console.log('save')
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(todos))
}
}, [todos])
So I have a workaround, but why is this necessary? I can't wrap my head around how this can be intended React functionality.
useEffect(() => {
console.log(`useEffect[todos]: ${JSON.stringify(todos)}`)
if (todos.length != 0) {
console.log('save')
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(todos))
}
}, [todos])
useEffect is always triggered initially, even though it has dependencies. In your case with the above snippet, it will be triggered twice:
Initial loading (like [] - no dependencies)
Updated todos state
So that's why it set empty data to localStorage because of initial loading without empty todos (if you don't have the condition todos.length != 0)
Your above snippet with the condition todos.length != 0 is reasonable, but it won't work for delete-all cases.
If you don't use any server-side rendering frameworks, you can set a default value for todos state
const [todos, setTodos] = useState(localStorage.getItem(LOCAL_STORAGE_KEY))
With this change, you can update useEffect like below
useEffect(() => {
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(todos))
}, [todos])
If you use a server-side rendering framework like NextJS, you can try to update localStorage directly on events (toggleTodo and handleAddTodo) instead of useEffect.
function App() {
const [todos, setTodos] = useState([])
const todoNameRef = useRef()
useEffect(() => {
console.log(`useEffect[]`)
const storedTodos = JSON.parse(localStorage.getItem(LOCAL_STORAGE_KEY));
if (storedTodos) {
console.log(`set todos to: ${JSON.stringify(storedTodos)}`)
setTodos(storedTodos)
// can't print here - value is set asynchronously
// console.log(`loaded todos: ${JSON.stringify(todos)}`)
}
}, [])
function toggleTodo(id) {
const newTodos = [...todos]
const todo = newTodos.find(todo => todo.id === id)
console.log(`toggleTodo: ${todo.name}`)
todo.complete = !todo.complete
setTodos(newTodos)
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(newTodos))
}
function handleAddTodo(e) {
const name = todoNameRef.current.value
console.log(`handleAddTodo: ${name}`)
// setTodos(todos.concat({completed: false, name: todoNameRef.current.value}))
if (name === '') {
return
}
const updatedTodos = [...todos, { id:uuidv4(), name:name, complete:false }]
setTodos(updatedTodos)
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(updatedTodos))
}
return (
<div>
<TodoList todos={todos} handleCheckboxChanged={toggleTodo}/>
<input ref={todoNameRef} type="text" />
<button onClick={handleAddTodo}>Add Todo</button>
<button>Clear Completed</button>
<div>0 left to do</div>
</div>
)
}

How can I make syntax less repetitive (DRY)?

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)

Resources