How to rerender with setState using the same value in MaterialUi Snackbar - reactjs

I have an Input and when the user submits a value which does not exist a not found message is shown for example setError("Not found") in the Toast (Snackbar) which closes after timeout. I am using Material UI and i am using it as its shown in this example Consecutive Snackbars in the docs. Only difference i made is i put this in a seperate component and added useEffect like this
const Toast = ({message}) => {
React.useEffect(() => {
if(message) {
setSnackPack((prev) => [...prev, { message, key: new Date().getTime() }]);
}
}, [message])
... // Rest of the code from the docs
}
And this works when the error is set but if the same errors occurs (same value) it wont show the value (Snackbar)like in the example as it will not rerender because it has the same value.
My question is how would i cause the rerender so that the same string appears again, or is there a different way I could do something like this as I feel this is an anti-pattern?

I'm not 100% sure I understood your problem, but I suspect you might not be managing the Snackbar's open state properly. I suggest making sure that it's always set (or inherited from the parent) correctly (e.g. not just set once upon state initialization).
The code below allows displaying a Snackbar with identical parameters multiple times. See this codesandbox for a fully working example.
import { useState } from "react";
import Snackbar from "#material-ui/core/Snackbar";
export default function App() {
const [text, setText] = useState();
const [show, setShow] = useState(false);
return (
<>
<input onChange={(e) => setText(e.target.value)} />
<button onClick={() => setShow(true)}>Show it</button>
<Toast show={show} hide={() => setShow(false)} message={text} />
</>
);
}
const Toast = ({ message, show, hide }) => {
return (
<Snackbar
open={show}
autoHideDuration={1000}
onClose={hide}
message={message}
/>
);
};

You could change message to an object
let message = {
messageText: "A bad thing happened...",
timestamp: 1626698144204
}
Then in your handler, update the timestamp. Then the useEffect should fire when the timestamp gets updated.

Related

React, how to make state work between childs?

I am trying to figure out how to make a searchFilter work. Here is my situation:
In App.js I hold a state for my items that i get from an api.
I also hold a state for the SearchFilter.
The items arrive and I can render them just fine.
Further, in App.js, I render the items and also a search component. So my code looks something like this:
const App = () => {
const [items, setItems] = useState([])
const [searchFilter, setSearchFilter] = useState("")
useEffect(() => {
const fetchItems = async () => {
// FETCHING ITEMS AND SETTING VIA setItems...
// This part works as expected
}
fetchItems()
},[])
return (
<>
<SearchBar setSearchFilter={setSearchFilter} />
<RenderItems items={items} searchFilter={searchFilter} />
</>
)
}
The problem I face is, that the searchFilter remains undefined in the RenderItems component. Why?
It gets updated correctly in App.js, but somehow doesn't make it's way to RenderItems
Inside component SearchBar:
const SearchBar = ({setSearchFilter}) => {
return (
<>
<input type="text" placeholder="Search" onChange={(e) => setSearchFilter(e.target.value) }/ >
</>
)
{
Any clues?
Thank you all for the replies #Mandeep Kaur and #KcH
I found the problem was in the data that came from the api when trying this scenario out in a codesandbox.
I keep the link here for future reference: https://codesandbox.io/s/nostalgic-booth-p1tqsv?file=/src/App.js
Closed from my side.
I think this happens because RenderItems component is not re-render after updating the state in SearchBar component.
You can try with adding one useEffect that makes it re-render and it gives the latest data to RenderItems
useEffect(() => {
},[searchFilter])
I am assuming that the updated value you getting in the App.js file.

Text input loses focus with useState event

I am building a React component that uses react-ace to input markdown that is rendered in real time. Every time I type a letter, the letter gets entered and then focus immediately changes and I can no longer type without clicking back into the editor.
Here is the code for the component:
export default function Edit(props) {
let placeHolder = props.isNew ? md : "";
let urlParams = useParams();
let navigate = useNavigate();
let action;
let actionName;
const [contents, setContents] = useState(placeHolder);
const [post, setPost] = useState({});
const [idToken, setIdToken] = useState(null)
Auth.currentSession().then((s) => setIdToken(s.idToken.jwtToken))
if (!props.isNew) {
useEffect(() => {
getPost(urlParams.postId)
.then(post => {
setContents(post.contents);
setPost(post);
});
}, [props.postId]);
action = () => {
updatePost(post, contents, idToken)
.then(() => navigate('/'))
}
actionName = "Update"
}
else {
action = () => {
newPost(contents, idToken)
.then(() => navigate('/'))
}
actionName = "Create"
}
return (
<Authenticator hideSignUp={true}>
<PageContainer single={true}>
<ActionMenu action={action} cancel={() => navigate('/')}>{actionName}</ActionMenu>
<ContentContainer>
<AceEditor
mode="markdown"
theme="monokai"
onChange={v => {setContents(v)}}
name="editor"
height="100%"
width="60vw"
value={contents}
/>
<MarkdownWrapper>{contents}</MarkdownWrapper>
</ContentContainer>
</PageContainer>
</Authenticator>
)
}
I noticed that removing the setContents() call from the onChange prop of the AceEditor component stops this from happening which leads me to believe that this component is being re-rendered every time I type, causing focus to change. I tried removing contents from the value prop in case that was causing the re-render, but nothing changed.
How can I stop this component from re-rendering whenever setContents() is called?
I'm noticing two issues:
You are making an api call here: Auth.currentSession().then((s) => setIdToken(s.idToken.jwtToken)) outside of a useEffect. For the token, it would seem you would want to only call that once when the component mounts, so you want to pass an empty array as the second argument. For more info: https://medium.com/nerd-for-tech/fetching-api-using-useeffect-hook-in-react-js-7b9b34e427ca
You're calling useEffect in an if statement which is against the rules of hooks. https://ru.react.js.org/docs/hooks-rules.html
Try fixing these issues and seeing if the problem goes away.

Excessive rerendering when interacting with global state in React Context

I'm building a Chat app, I'm using ContextAPI to hold the state that I'll be needing to access from different unrelated components.
A lot of rerendering is happening because of the context, everytime I type a letter in the input all the components rerender, same when I toggle the RightBar which its state also resides in the context because I need to toggle it from a button in Navbar.
I tried to use memo on every components, still all the components rerender everytime I interact with state in context from any component.
I added my whole code simplified to this sandbox link : https://codesandbox.io/s/interesting-sky-fzmc6
And this is a deployed Netlify link : https://csb-fzmc6.netlify.app/
I tried to separate my code into some custom hooks like useChatSerice, useUsersService to simplify the code and make the actual components clean, I'll also appreciate any insight about how to better structure those hooks and where to put CRUD functions while avoiding the excessive rerendering.
I found some "solutions" indicating that using multiple contexts should help, but I can't figure out how to do this in my specific case, been stuck with this problem for a week.
EDIT :
The main problem here is a full rerender with every letter typed in the input.
The second, is the RightBar toggle button which also causes a full rerender.
Splitting the navbar and chat state into two separate React contexts is actually the recommended method from React. By nesting all the state into a new object reference anytime any single state updated it necessarily triggers a rerender of all consumers.
<ChatContext.Provider
value={{ // <-- new object reference each render
rightBarValue: [rightBarIsOpen, setRightBarIsOpen],
chatState: {
editValue,
setEditValue,
editingId,
setEditingId,
inputValue,
setInputValue,
},
}}
>
{children}
</ChatContext.Provider>
I suggest carving rightBarValue and state setter into its own context.
NavBar context
const NavBarContext = createContext([false, () => {}]);
const NavBarProvider = ({ children }) => {
const [rightBarIsOpen, setRightBarIsOpen] = useState(true);
return (
<NavBarContext.Provider value={[rightBarIsOpen, setRightBarIsOpen]}>
{children}
</NavBarContext.Provider>
);
};
const useNavBar = () => useContext(NavBarContext);
Chat context
const ChatContext = createContext({
editValue: "",
setEditValue: () => {},
editingId: null,
setEditingId: () => {},
inputValue: "",
setInputValue: () => {}
});
const ChatProvider = ({ children }) => {
const [inputValue, setInputValue] = useState("");
const [editValue, setEditValue] = useState("");
const [editingId, setEditingId] = useState(null);
const chatState = useMemo(
() => ({
editValue,
setEditValue,
editingId,
setEditingId,
inputValue,
setInputValue
}),
[editValue, inputValue, editingId]
);
return (
<ChatContext.Provider value={chatState}>{children}</ChatContext.Provider>
);
};
const useChat = () => {
return useContext(ChatContext);
};
MainContainer
const MainContainer = () => {
return (
<ChatProvider>
<NavBarProvider>
<Container>
<NavBar />
<ChatSection />
</Container>
</NavBarProvider>
</ChatProvider>
);
};
NavBar - use the useNavBar hook
const NavBar = () => {
const [rightBarIsOpen, setRightBarIsOpen] = useNavBar();
useEffect(() => {
console.log("NavBar rendered"); // <-- log when rendered
});
return (
<NavBarContainer>
<span>MY NAVBAR</span>
<button onClick={() => setRightBarIsOpen(!rightBarIsOpen)}>
TOGGLE RIGHT-BAR
</button>
</NavBarContainer>
);
};
Chat
const Chat = ({ chatLines }) => {
const { addMessage, updateMessage, deleteMessage } = useChatService();
const {
editValue,
setEditValue,
editingId,
setEditingId,
inputValue,
setInputValue
} = useChat();
useEffect(() => {
console.log("Chat rendered"); // <-- log when rendered
});
return (
...
);
};
When running the app notice now that "NavBar rendered" only logs when toggling the navbar, and "Chat rendered" only logs when typing in the chat text area.
I recommend use jotai or other state management libraries.
Context is not suitable for high-frequency changes.
And, the RightBar's state looks can separate to other hook/context.
There is tricky one solution solve some render problems:
https://codesandbox.io/s/stoic-mclaren-x6yfv?file=/src/context/ChatContext.js
Your code needs to be refactored, and useChatService in ChatSection also depends on your useChat, so ChatSection will re-render when the text changes.
It looks like you are changing a global context on input field data change. If your global context is defined on a level of parent components (in relation to your input component), then the parent and all children will have to re-render.
You have several options to avoid this behavior:
Use context on a lower level, e.g. by extracting your input field to an external component and using useContext hook there
Save the input to local state of a component and only sync it to the global context on blur or submit

Is it Encouraged to Use Closures to "Save" Values within React Hooks

Here is some code to illustrate what I am trying to accomplish:
import React, { useState } from "react";
export default function App() {
const [list, setList] = useState(["a", "b", "c"]);
const [text, setText] = useState("");
let listBefore;
const addItem = () => {
listBefore = list;
setList([text, ...list]);
//simulate api call "addNewItem(newItem, confirmAdd)"
setTimeout(() => confirmAdd(false), 2000);
};
const confirmAdd = success => {
if (!success) {
setList(listBefore);
}
};
return (
<div className="App">
{list.toString()}
<input onChange={e => setText(e.target.value)} />
<button onClick={addItem}>Add</button>
</div>
);
}
The goal is to immediately update the UI before I know that the new list item was successfully added to the database. In the off chance that it failed, I revert back using my listBefore variable. I have been trying to figure out why this works and stumbled across closure. My guess is that, even though many rerenders may occur before the request succeeds (and listBefore is recreated as undefined), listBefore is "remembered" inside confirmAdd() because it had the value at the time of being called.
So I am wondering:
Am I correct in understanding how closure is being applied?
Is this encouraged?
Yes your thought of closure is correct.
It works but it is not encouraged, because this is hard to understand, and tracking when reading or debugging code. You should use useRef to sotre beforeList in this case.

React useState Hooks - managing multiple loading states within a state

I am trying to manage multiple loading states inside a state dynamically with react hooks.
But, when I update the state object depending the result of an api call (success or error), it produces a very odd result.
As in my code example below, a button click will trigger the loading state for the button to be true.
When I click button 1 and then button 2 while button 1 is making a call (here I just simulated with timeout), they both set their loading states to true in order as expected.
Then, after some time, both loading states are set to false.
But, then both call are finished, button 1 loading state sets to true again...!?
I am not sure how I can solve this issue, and your help is appreciated.
Thank you.
https://codesandbox.io/s/usestate-async-timeout-issue-oknvu
import React, { useState } from "react";
import ReactDOM from "react-dom";
import "./styles.css";
function App() {
const [loading, setLoading] = useState({});
const handleOnClick = id => {
setLoading({ ...loading, [id]: true });
setTimeout(() => {
// simulate the case for when error is returned after an api call.
setLoading({ ...loading, [id]: false });
}, 2000);
};
return (
<div className="App">
<h1>React UseState: Updating a state object</h1>
{["1", "2"].map(id => {
return !loading[id] ? (
<button key={id} onClick={() => handleOnClick(id)}>
Item {id}
</button>
) : (
"Loading..."
);
})}
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
State updates are not synchronous in React, this is a common problem when you're updating state based on the previous state. I think you need to do the following change:
const handleOnClick = id => {
setLoading(prev => ({ ...prev, [id]: true }));
setTimeout(() => {
// simulate the case for when error is returned after an api call.
setLoading(prev => ({ ...prev, [id]: false }));
}, 2000);
};
I've changed both the setLoading lines to use the previous state in their updates.
You can read more about this here:
https://reactjs.org/docs/hooks-reference.html#functional-updates
When to use React setState callback
I think this explains the problem you are facing: https://reactjs.org/docs/hooks-faq.html#why-am-i-seeing-stale-props-or-state-inside-my-function

Resources