I've run into an issue while trying to build a page that allows the user to click on a word and get its definition in a bootstrap popover. That is achieved by sending an API request and updating the state with the received data.
The problem is that the popover only appears after the second click on the word. The console.log() in useEffect() shows that every time a new word is clicked an API request is made. For the popover to appear the same word must be clicked twice. It'd be better if it only took one click.
import React, { useState, useRef, useEffect } from "react";
import axios from "axios";
import { Alert, Popover, OverlayTrigger } from "react-bootstrap";
export default function App() {
const [text, setText] = useState(
"He looked at her and saw her eyes luminous with pity."
);
const [selectedWord, setSelectedWord] = useState("luminous");
const [apiData, setApiData] = useState([
{
word: "",
phonetics: [{ text: "" }],
meanings: [{ definitions: [{ definition: "", example: "" }] }]
}
]);
const words = text.split(/ /g);
useEffect(() => {
var url = "https://api.dictionaryapi.dev/api/v2/entries/en/" + selectedWord;
axios
.get(url)
.then(response => {
setApiData(response.data)
console.log("api call")
})
.catch(function (error) {
if (error) {
console.log("Error", error.message);
}
});
}, [selectedWord]);
function clickCallback(w) {
var word = w.split(/[.!?,]/g)[0];
setSelectedWord(word);
}
const popover = (
<Popover id="popover-basic">
<Popover.Body>
<h1>{apiData[0].word}</h1>
<h6>{apiData[0].meanings[0].definitions[0].definition}</h6>
</Popover.Body>
</Popover>
);
return (
<Alert>
{words.map((w) => (
<OverlayTrigger
key={uuid()}
trigger="click"
placement="bottom"
overlay={popover}
>
<span onClick={() => clickCallback(w)}> {w}</span>
</OverlayTrigger>
))}
</Alert>
);
}
UPDATE:
Changed the apiData initialization and the <Popover.Body> component. That hasn't fixed the problem.
const [apiData, setApiData] = useState(null)
<Popover.Body>
{
apiData ?
<div>
<h1>{apiData[0].word}</h1>
<h6>{apiData[0].meanings[0].definitions[0].definition}</h6>
</div> :
<div>Loading...</div>
}
</Popover.Body>
The Problem
Here's what I think is happening:
Component renders
Start fetching definition for "luminous".
The definition of "luminous" has finished being fetched. It calls setApiData(data).
Component rerenders
If you click "luminous", the popper is shown immediately, this is because the data for the popper is ready to use and setSelectedWord("luminous") does nothing.
If you click another word, such as "pity", the popper attempts to show, but setSelectedWord("pity") causes the component to start rerendering.
Component rerenders
Start fetching definition for "pity".
The definition of "pity" has finished being fetched. It calls setApiData(data).
Component rerenders
If you click "pity", the popper is shown immediately, this is because the data for the popper is ready to use and setSelectedWord("pity") does nothing.
Selecting another word will repeat this process over and over.
To fix this, you need to first make use of the show property to show the popover after rendering it out if it matches the selected word. But what if the word appears multiple times? If you did this for the word "her", it would show the popover in multiple places. So instead of comparing against each word, you'd have to assign each word a unique ID and compare against that.
Fixing the Component
To assign words an ID that won't change between renders, we need to assign them IDs at the top of your component and store them in an array. To make this "simpler", we can abstract that logic into a re-useable function outside of your component:
// Use this function snippet in demos only, use a more robust package
// https://gist.github.com/jed/982883 [DWTFYWTPL]
const uuid = function b(a){return a?(a^Math.random()*16>>a/4).toString(16):([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g,b)}
// Splits the text argument into words, removes excess formatting characters and assigns each word a UUID.
// Returns an array with the shape: { [index: number]: { word: string, original: string, uuid: string }, text: string }
function identifyWords(text) {
// split input text into words with unique Ids
const words = text
.split(/ +/)
.map(word => {
const cleanedWord = word
.replace(/^["]+/, "") // remove leading punctuation
.replace(/[.,!?"]+$/, "") // remove trailing punctuation
return { word: cleanedWord, original: word, uuid: uuid() }
});
// attach the source text to the array of words
// we can use this to prevent unnecessary rerenders
words.text = text;
// return the array-object
return words;
}
Within the component, we need to setup the state variables to hold the words array. By passing a callback to useState, React will only execute it on the first render and skip calling it on rerenders.
// set up state array of words that have their own UUIDs
// note: we don't want to call _setWords directly
const [words, _setWords] = useState(() => identifyWords("He looked at her and saw her eyes luminous with pity."));
Now that we have words and _setWords, we can pull out the text value from it:
// extract text from words array for convenience
// probably not needed
const text = words.text;
Next, we can create our own setText callback. This could be simpler, but I wanted to make sure we support React's mutating update syntax (setText(oldValue => newValue)):
// mimic a setText callback that actually updates words as needed
const setText = (newTextOrCallback) => {
if (typeof newTextOrCallback === "function") {
// React mutating callback mode
_setWords((words) => {
const newText = newTextOrCallback(words.text);
return newText === words.text
? words // unchanged
: identifyWords(newText); // new value
});
} else {
// New value mode
return newTextOrCallback === words.text
? words // unchanged
: identifyWords(newTextOrCallback); // new value
}
}
Next, we need to set up the currently selected word. Once the definition is available, this word's popover will be shown.
const [selectedWordObj, setSelectedWordObj] = useState(() => words.find(({word}) => word === "luminous"));
If you don't want to show a word by default, use:
const [selectedWordObj, setSelectedWordObj] = useState(); // nothing selected by default
To fix the API call, we need to make use of the "use async effect" pattern (there are libraries out there to simplify this):
const [apiData, setApiData] = useState({ status: "loading" });
useEffect(() => {
if (!selectedWordObj) return; // do nothing.
// TODO: check cache here
// clear out the previous definition
setApiData({ status: "loading" });
let unsubscribed = false;
axios
.get(`https://api.dictionaryapi.dev/api/v2/entries/en/${selectedWordObj.word}`)
.then(response => {
if (unsubscribed) return; // do nothing. out of date response
const body = response.data;
// unwrap relevant bits
setApiData({
status: "completed",
word: body.word,
definition: body.meanings[0].definitions[0].definition
});
})
.catch(error => {
if (unsubscribed) return; // do nothing. out of date response
console.error("Failed to get definition: ", error);
setApiData({
status: "error",
word: selectedWordObj.word,
error
});
});
return () => unsubscribed = true;
}, [selectedWord]);
The above code block makes sure to prevent calling the setApiData methods when they aren't needed any more. It also uses a status property to track it's progress so you can render the result properly.
Now to define a popover that shows a loading message:
const loadingPopover = (
<Popover id="popover-basic">
<Popover.Body>
<span>Loading...</span>
</Popover.Body>
</Popover>
);
We can mix that loading popover with apiData to get a popover to show the definition. If we're still loading the definition, use the loading one. If we've had an error, show the error. If it completed properly, render out the defintion. To make this easier, we can put this logic in a function outside of your component like so:
function getPopover(apiData, loadingPopover) {
switch (apiData.status) {
case "loading":
return loadingPopover;
case "error":
return (
<Popover id="popover-basic">
<Popover.Body>
<h1>{apiData.word}</h1>
<h6>Couldn't find definition for {apiData.word}: {apiData.error.message}</h6>
</Popover.Body>
</Popover>
);
case "completed":
return (
<Popover id="popover-basic">
<Popover.Body>
<h1>{apiData.word}</h1>
<h6>{apiData.definition}</h6>
</Popover.Body>
</Popover>
);
}
}
We call this funtion in the component using:
const selectedWordPopover = getPopover(apiData, loadingPopover);
Finally, we render out the words. Because we are rendering out an array, we need to use a key property that we'll set to each word's Id. We also need to select the word that was clicked - even if there were more than one of the same words, we only want just the clicked one. For that we'll check its Id too. If we click on a particular word, we need to sure that the one we clicked on is selected. We also need to render out the original word with its punctuation. This is all done in this block:
return (
<Alert>
{words.map((wordObj) => {
const isSelectedWord = selectedWordObj && selectedWordObj.uuid = wordObj.uuid;
return (
<OverlayTrigger
key={wordObj.uuid}
show={isSelectedWord}
trigger="click"
placement="bottom"
overlay={isSelectedWord ? selectedWordPopover : loadingPopover}
>
<span onClick={() => setSelectedWordObj(wordObj)}> {wordObj.original}</span>
</OverlayTrigger>
)})}
</Alert>
);
Complete Code
Bringing all that together gives:
import React, { useState, useRef, useEffect } from "react";
import axios from "axios";
import { Alert, Popover, OverlayTrigger } from "react-bootstrap";
// Use this function snippet in demos only, use a more robust package
// https://gist.github.com/jed/982883 [DWTFYWTPL]
const uuid = function b(a){return a?(a^Math.random()*16>>a/4).toString(16):([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g,b)}
// Splits the text argument into words, removes excess formatting characters and assigns each word a UUID.
// Returns an array with the shape: { [index: number]: { word: string, original: string, uuid: string }, text: string }
function identifyWords(text) {
// split input text into words with unique Ids
const words = text
.split(/ +/)
.map(word => {
const cleanedWord = word
.replace(/^["]+/, "") // remove leading characters
.replace(/[.,!?"]+$/, "") // remove trailing characters
return { word: cleanedWord, original: word, uuid: uuid() }
});
// attach the source text to the array of words
words.text = text;
// return the array
return words;
}
function getPopover(apiData, loadingPopover) {
switch (apiData.status) {
case "loading":
return loadingPopover;
case "error":
return (
<Popover id="popover-basic">
<Popover.Body>
<h1>{apiData.word}</h1>
<h6>Couldn't find definition for {apiData.word}: {apiData.error.message}</h6>
</Popover.Body>
</Popover>
);
case "completed":
return (
<Popover id="popover-basic">
<Popover.Body>
<h1>{apiData.word}</h1>
<h6>{apiData.definition}</h6>
</Popover.Body>
</Popover>
);
}
}
export default function App() {
// set up state array of words that have their own UUIDs
// note: don't call _setWords directly
const [words, _setWords] = useState(() => identifyWords("He looked at her and saw her eyes luminous with pity."));
// extract text from words array for convenience
const text = words.text;
// mimic a setText callback that actually updates words as needed
const setText = (newTextOrCallback) => {
if (typeof newTextOrCallback === "function") {
// React mutating callback mode
_setWords((words) => {
const newText = newTextOrCallback(words.text);
return newText === words.text
? words // unchanged
: identifyWords(newText); // new value
});
} else {
// New value mode
return newTextOrCallback === words.text
? words // unchanged
: identifyWords(newTextOrCallback); // new value
}
}
const [selectedWordObj, setSelectedWordObj] = useState(() => words.find(({word}) => word === "luminous"));
const [apiData, setApiData] = useState({ status: "loading" });
useEffect(() => {
if (!selectedWordObj) return; // do nothing.
// TODO: check cache here
// clear out the previous definition
setApiData({ status: "loading" });
let unsubscribed = false;
axios
.get(`https://api.dictionaryapi.dev/api/v2/entries/en/${selectedWordObj.word}`)
.then(response => {
if (unsubscribed) return; // do nothing. out of date response
const body = response.data;
// unwrap relevant bits
setApiData({
status: "completed",
word: body.word,
definition: body.meanings[0].definitions[0].definition
});
})
.catch(error => {
if (unsubscribed) return; // do nothing. out of date response
console.error("Failed to get definition: ", error);
setApiData({
status: "error",
word: selectedWordObj.word,
error
});
});
return () => unsubscribed = true;
}, [selectedWord]);
function clickCallback(w) {
var word = w.split(/[.!?,]/g)[0];
setSelectedWord(word);
}
const loadingPopover = (
<Popover id="popover-basic">
<Popover.Body>
<span>Loading...</span>
</Popover.Body>
</Popover>
);
const selectedWordPopover = getPopover(apiData, loadingPopover);
return (
<Alert>
{words.map((wordObj) => {
const isSelectedWord = selectedWordObj && selectedWordObj.uuid = wordObj.uuid;
return (
<OverlayTrigger
key={wordObj.uuid}
show={isSelectedWord}
trigger="click"
placement="bottom"
overlay={isSelectedWord ? selectedWordPopover : loadingPopover}
>
<span onClick={() => setSelectedWordObj(wordObj)}> {wordObj.original}</span>
</OverlayTrigger>
)})}
</Alert>
);
}
Note: You can improve this by caching the results from the API call.
Related
So I'm trying to centralize some alert-related logic in my app in a single .tsx file, that needs to be available in many components (specfically, an "add alert" fuction that will be called from many components). To this end I am trying to use react context to make the alert logic available, with the state (an array of active alerts) stored in App.tsx.
Alerts.tsx
export interface AlertContext {
alerts: Array<AppAlert>,
addAlert: (msg: React.ReactNode, style: string, callback?: (id: string) => {}) => void,
clearAlert: (id: string) => void
}
[...]
export function AlertsProvider(props: AlertsProps) {
function clearAlert(id: string){
let timeout = props.currentAlerts.find(t => t.id === id)?.timeout;
if(timeout){
clearTimeout(timeout);
}
let newCurrent = props.currentAlerts.filter(t => t.id != id);
props.setCurrentAlerts(newCurrent);
}
function addAlert(msg: JSX.Element, style: string, callback: (id: string) => {}) {
console.log("add alert triggered");
let id = uuidv4();
let newTimeout = setTimeout(clearAlert, timeoutMilliseconds, id);
let newAlert = {
id: id,
msg: msg,
style: style,
callback: callback,
timeout: newTimeout
} as AppAlert;
let test = [...props.currentAlerts, newAlert];
console.log(test);
props.setCurrentAlerts(test);
console.log("current alerts", props.currentAlerts);
}
let test = {
alerts: props.currentAlerts,
addAlert: addAlert,
clearAlert: clearAlert
} as AlertContext;
return (<AlertsContext.Provider value={test}>
{ props.children }
</AlertsContext.Provider>);
}
App.tsx
function App(props: AppProps){
[...]
const [currentAlerts, setCurrentAlerts] = useState<Array<AppAlert>>([]);
[...]
const alertsContext = useContext(AlertsContext);
console.log("render app", alertsContext.alerts);
return (
<AlertsProvider currentAlerts={currentAlerts} setCurrentAlerts={setCurrentAlerts}>
<div className={ "app-container " + (error !== undefined ? "err" : "") } >
{ selectedMode === "Current" &&
<CurrentItems {...currentItemsProps} />
}
{ selectedMode === "History" &&
<History {...historyProps } />
}
{ selectedMode === "Configure" &&
<Configure {...globalProps} />
}
</div>
<div className="footer-container">
{
alertsContext.alerts.map(a => (
<Alert variant={a.style} dismissible transition={false} onClose={a.callback}>
{a.msg}
</Alert>
))
}
{/*<Alert variant="danger" dismissible transition={false}
show={ error !== undefined }
onClose={ dismissErrorAlert }>
<span>{ error?.msg }</span>
</Alert>*/}
</div>
</AlertsProvider>
);
}
export default App;
I'm calling alertsContext.addAlert in only one place in CurrentItems.tsx so far. I've also added in some console statements for easier debugging. The output in the console is as follows:
render app Array [] App.tsx:116
XHRGEThttp://localhost:49153/currentitems?view=Error [HTTP/1.1 500 Internal Server Error 1ms]
Error 500 fetching current items for view Error: Internal Server Error CurrentItems.tsx:94
add alert triggered Alerts.tsx:42
Array [ {…}, {…} ] Alerts.tsx:53
current alerts Array [ {…} ] Alerts.tsx:55
render app Array []
So I can see that by the end of the addAlert function the currentAlerts property appears to have been updated, but then subsequent console statement in the App.tsx shows it as empty. I'm relatively new to React, so I'm probably having some misunderstanding of how state is meant to be used / function, but I've been poking at this on and off for most of a day with no success, so I'm hoping someone can set me straight.
const alertsContext = useContext(AlertsContext);
This line in App is going to look for a provider higher up the component tree. There's a provider inside of App, but that doesn't matter. Since there's no provider higher in the component tree, App is getting the default value, which never changes.
You will either need to invert the order of your components, so the provider is higher than the component that's trying to map over the value, or since the state variable is already in App you could just use that directly and delete the call to useContext:
function App(props: AppProps){
[...]
const [currentAlerts, setCurrentAlerts] = useState<Array<AppAlert>>([]);
[...]
// Delete this line
// const alertsContext = useContext(AlertsContext);
console.log("render app", currentAlerts);
[...]
{
currentAlerts.map(a => (
<Alert variant={a.style} dismissible transition={false} onClose={a.callback}>
{a.msg}
</Alert>
))
}
}
Im trying to make a hook similar to Waypoint.
I simply want to load items and then when the waypoint is out of screen, allow it to load more items if the waypoint is reached.
I can't seem to figure out the logic to have this work properly.
Currently it see the observer state that its on the screen. then it fetches data rapidly.
I think this is because the hook starts at false everytime. Im not sure how to make it true so the data can load. Followed by the opposite when its reached again.
Any ideas.
Here's the hook:
import { useEffect, useState, useRef, RefObject } from 'react';
export default function useOnScreen(ref: RefObject<HTMLElement>) {
const observerRef = useRef<IntersectionObserver | null>(null);
const [isOnScreen, setIsOnScreen] = useState(false);
useEffect(() => {
observerRef.current = new IntersectionObserver(([entry]) => {
if (isOnScreen !== entry.isIntersecting) {
setIsOnScreen(entry.isIntersecting);
}
});
}, []);
useEffect(() => {
observerRef.current.observe(ref.current);
return () => {
observerRef.current.disconnect();
};
}, [ref]);
return isOnScreen;
}
Here's the use of it:
import React, { useRef } from 'react';
import { WithT } from 'i18next';
import useOnScreen from 'utils/useOnScreen';
interface IInboxListProps extends WithT {
messages: any;
fetchData: () => void;
searchTerm: string;
chatID: string | null;
}
const InboxList: React.FC<IInboxListProps> = ({ messages, fetchData, searchTerm, chatID}) => {
const elementRef = useRef(null);
const isOnScreen = useOnScreen(elementRef);
if (isOnScreen) {
fetchData();
}
const renderItem = () => {
return (
<div className='item unread' key={chatID}>
Item
</div>
);
};
const renderMsgList = ({ messages }) => {
return (
<>
{messages.map(() => {
return renderItem();
})}
</>
);
};
let messagesCopy = [...messages];
//filter results
if (searchTerm !== '') {
messagesCopy = messages.filter(msg => msg.user.toLocaleLowerCase().startsWith(searchTerm.toLocaleLowerCase()));
}
return (
<div className='conversations'>
{renderMsgList({ messages: messagesCopy })}
<div className='item' ref={elementRef} style={{ bottom: '10%', position: 'relative',backgroundColor:"blue",width:"5px",height:"5px" }} />
</div>
);
};
export default InboxList;
Let's inspect this piece of code
const [isOnScreen, setIsOnScreen] = useState(false);
useEffect(() => {
observerRef.current = new IntersectionObserver(([entry]) => {
if (isOnScreen !== entry.isIntersecting) {
setIsOnScreen(entry.isIntersecting);
}
});
}, []);
We have the following meanings:
.isIntersecting is TRUE --> The element became visible
.isIntersecting is FALSE --> The element disappeared
and
isOnScreen is TRUE --> The element was at least once visible
isOnScreen is FALSE--> The element was never visible
When using a xor (!==) you specify that it:
Was never visible and just became visible
this happens 1 time just after the first intersection
Was visible once and now disappeared
this happens n times each time the element is out of the screen
What you want to do is to get more items each time the element intersects
export default function useOnScreen(ref: RefObject<HTMLElement>, onIntersect: function) {
const observerRef = useRef<IntersectionObserver | null>(null);
const [isOnScreen, setIsOnScreen] = useState(false);
useEffect(() => {
observerRef.current = new IntersectionObserver(([entry]) => {
setIsOnScreen(entry.isIntersecting);
});
}, []);
useEffect(()=?{
if(isOnScreen){
onIntersect();
}
},[isOnScreen,onIntersect])
...
}
and then use it like:
const refetch= useCallback(()=>{
fetchData();
},[fetchData]);
const isOnScreen = useOnScreen(elementRef, refetch);
or simply:
const isOnScreen = useOnScreen(elementRef, fetchData);
If fetchData changes reference for some reason, you might want to use the following instead:
const refetch= useRef(fetchData);
const isOnScreen = useOnScreen(elementRef, refetch);
Remember that useOnScreen has to call it like onIntersect.current()
In InboxList component, what we are saying by this code
if (isOnScreen) {
fetchData();
}
is that, every time InboxList renders, if waypoint is on screen, then initiate the fetch, regardless of whether previous fetch is still in progress.
Note that InboxList could get re-rendered, possibly multiple times, while the fetch is going on, due to many reasons e.g. parent component re-rendering. Every re-rendering will initiate new fetch as long as waypoint is on screen.
To prevent this, we need to keep track of ongoing fetch, something like typical isLoading state variable. Then initiate new fetch only if isLoading === false && isOnScreen.
Alternatively, if it is guaranteed that every fetch will push the waypoint off screen, then we can initiate the fetch only when waypoint is coming on screen, i.e. isOnScreen is changing to true from false :
useEffect(() => {
if (isOnScreen) {
fetchData();
}
}, [isOnScreen]);
However, this will not function correctly if our assumption, that the waypoint goes out of screen on every fetch, does not hold good. This could happen because
pageSize of fetch small and display area can accommodate more
elements
data received from a fetch is getting filtered out due to
client side filtering e.g. searchTerm.
As my assumption. Also you can try this way.
const observeRef = useRef(null);
const [isOnScreen, setIsOnScreen] = useState(false);
const [prevY, setPrevY] = useState(0);
useEffect(()=>{
fetchData();
var option = {
root : null,
rootmargin : "0px",
threshold : 1.0 };
const observer = new IntersectionObserver(
handleObserver(),
option
);
const handleObserver = (entities, observer) => {
const y = observeRef.current.boundingClientRect.y;
if (prevY > y) {
fetchData();
}
setPrevY(y);
}
},[prevY]);
In this case we not focus chat message. we only focus below the chat<div className="item element. when div element trigger by scroll bar the fetchData() calling again and again..
Explain :
In this case we need to use IntersectionObserver for read the element position. we need to pass two parameter for IntersectionObserver.
-first off all in the hanlderObserver you can see boundingClientRect.y. the boundingClientRect method read the element postion. In this case we need only y axis because use y.
when the scrollbar reach div element, y value changed. and then fetchData() is trigger again.
root : This is the root to use for the intersection. rootMargin : Just like a margin property, which is used to provide the margin value to the root either in pixel or in percent (%) . threshold : The number which is used to trigger the callback once the intersection’s area changes to be greater than or equal to the value we have provided in this example .
finally you can add loading status for loading data.
return (
<div className='conversations'>
{renderMsgList({ messages: messagesCopy })}
<div className='item' ref={observeRef} style={{ bottom: '10%', position: 'relative',backgroundColor:"blue",width:"5px",height:"5px" }} />
</div>
);
};
I hope its correct, i'm not sure. may it's helpful someone. thank you..
My image component displays images with a heart over it every time a user submits a search. The heart changes colors if the image is clicked, and should reset to white (default color) when user submits a new search. For some reason, the clicked-color persists even after a search. What am I not understanding about react states? This isn't simply something that changes on the next render. It just stays like that until I change it manually.
const Image = ({image, toggleFav, initialIcon, initialAlt}) => {
const [fav, setFav] = useState(false);
const [heartIcon, setHeartIcon] = useState(initialIcon)
const [heartAlt, setHeartAlt] = useState(initialAlt)
const handleClick = () => {
setFav(fav => !fav);
toggleFav(image.id, fav);
if (heartIcon == "whiteHeartIcon") {
setHeartIcon("redHeartIcon")
}
else {
setHeartIcon("whiteHeartIcon")
}
if (heartAlt == "white heart icon") {
setHeartAlt("red heart icon")
}
else {
setHeartAlt("white heart icon")
}
};
return (
<Grid item xs={4} key={image.id}>
<div className={`${fav ? "fav" : ""}`} onClick={handleClick}>
<div className="imgBox">
<img src={image.url} className="image"/>
<Heart icon={heartIcon} alt={heartAlt} className="heart"/>
</div>
</div>
</Grid>
);
}
This is the handle submit func for the component:
const searchAllImages = async (keyword) => {
const response = await searchImages(keyword);
const imageObjects = response.data.message.map((link, index) => {
let newImage = {
url: link,
id: link,
fav: false
};
return newImage;
});
dispatch({type: 'SET_IMAGES', payload: imageObjects});
};
I render the images through a redux store where it replaces the image state every time a new search is done. The state resides in Store.js where image is initially set to an empty list. The dispatch method comes from Reducer.js where the method is defined.
case "SET_IMAGES":
return {
...state,
images: action.payload
}
Have you tried setting the initial image to a different variable initially, then use a useEffect that checks the image variable for changes, and if it changes assign the value to the variable mentioned above. Anyways more like using useEffect or useCallback for actions.
I am looking into fixing a bug in the code. There is a form with many form fields. Project Name is one of them. There is a button next to it.So when a user clicks on the button (plus icon), a popup window shows up, user enters Project Name and Description and hits submit button to save the project.
The form has Submit, Reset and Cancel button (not shown in the code for breviety purpose).
The project name field of the form has auto suggest feature. The code snippet below shows the part of the form for Project Name field.So when a user starts typing, it shows the list of projects
and user can select from the list.
<div id="formDiv">
<Growl ref={growl}/>
<Form className="form-column-3">
<div className="form-field project-name-field">
<label className="MuiFormLabel-root MuiInputLabel-root MuiInputLabel-animated custom-label">Project Name</label>
<AutoProjects
fieldName='projectId'
value={values.projectId}
onChange={setFieldValue}
error={errors.projects}
touched={touched.projects}
/>{touched.projects && errors.v && <Message severity="error" text={errors.projects}/>}
<Button className="add-project-btn" title="Add Project" variant="contained" color="primary"
type="button" onClick={props.addProject}><i className="pi pi-plus" /></Button>
</div>
The problem I am facing is when some one creates a new project. Basically, the autosuggest list is not showing the newly added project immediately after adding/creating a new project. In order to see the newly added project
in the auto suggest list, after creating a new project,user would have to hit cancel button of the form and then open the same form again. In this way, they can see the list when they type ahead to search for the project they recently
created.
How should I make sure that the list gets immediately updated as soon as they have added the project?
Below is how my AutoProjects component looks like that has been used above:
import React, { Component } from 'react';
import Autosuggest from 'react-autosuggest';
import axios from "axios";
import { css } from "#emotion/core";
import ClockLoader from 'react-spinners/ClockLoader'
function escapeRegexCharacters(str) {
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
// Use your imagination to render suggestions.
const renderSuggestion = suggestion => (
<div>
{suggestion.name}, {suggestion.firstName}
</div>
);
const override = css`
display: block;
margin: 0 auto;
border-color: red;
`;
export class AutoProjects extends Component {
constructor(props) {
super(props);
this.state = {
value: '',
projects: [],
suggestions: [],
loading: false
}
this.getSuggestionValue = this.getSuggestionValue.bind(this)
this.setAutoSuggestValue = this.setAutoSuggestValue.bind(this)
}
// Teach Autosuggest how to calculate suggestions for any given input value.
getSuggestions = value => {
const escapedValue = escapeRegexCharacters(value.trim());
if (escapedValue === '') {
return [];
}
const regex = new RegExp(escapedValue, 'i');
const projectData = this.state.projects;
if (projectData) {
return projectData.filter(per => regex.test(per.name));
}
else {
return [];
}
};
// When suggestion is clicked, Autosuggest needs to populate the input
// based on the clicked suggestion. Teach Autosuggest how to calculate the
// input value for every given suggestion.
getSuggestionValue = suggestion => {
this.props.onChange(this.props.fieldName, suggestion.id)//Update the parent with the new institutionId
return suggestion.name;
}
fetchRecords() {
const loggedInUser = JSON.parse(sessionStorage.getItem("loggedInUser"));
return axios
.get("api/projects/search/getProjectSetByUserId?value="+loggedInUser.userId)//Get all personnel
.then(response => {
return response.data._embedded.projects
}).catch(err => console.log(err));
}
setAutoSuggestValue(response) {
let projects = response.filter(per => this.props.value === per.id)[0]
let projectName = '';
if (projects) {
projectName = projects.name
}
this.setState({ value: projectName})
}
componentDidMount() {
this.setState({ loading: true}, () => {
this.fetchRecords().then((response) => {
this.setState({ projects: response, loading: false }, () => this.setAutoSuggestValue(response))
}).catch(error => error)
})
}
onChange = (event, { newValue }) => {
this.setState({
value: newValue
});
};
// Autosuggest will call this function every time you need to update suggestions.
// You already implemented this logic above, so just use it.
onSuggestionsFetchRequested = ({ value }) => {
this.setState({
suggestions: this.getSuggestions(value)
});
};
// Autosuggest will call this function every time you need to clear suggestions.
onSuggestionsClearRequested = () => {
this.setState({
suggestions: []
});
};
render() {
const { value, suggestions } = this.state;
// Autosuggest will pass through all these props to the input.
const inputProps = {
placeholder: value,
value,
onChange: this.onChange
};
// Finally, render it!
return (
<div>
<Autosuggest
suggestions={suggestions}
onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
onSuggestionsClearRequested={this.onSuggestionsClearRequested}
getSuggestionValue={this.getSuggestionValue}
renderSuggestion={renderSuggestion}
inputProps={inputProps}
/>
<div className="sweet-loading">
<ClockLoader
css={override}
size={50}
color={"#123abc"}
loading={this.state.loading}
/>
</div>
</div>
);
}
}
The problem is you only call the fetchRecord when component AutoProjects did mount. That's why whenever you added a new project, the list didn't update. It's only updated when you close the form and open it again ( AutoProjects component mount again)
For this case I think you should lift the logic of fetchProjects to parent component and past the value to AutoProjects. Whenever you add new project you need to call the api again to get a new list.
I'm trying to take an e.target.value which is an icon and filter it out from an array in state, and re-render the new state minus the matching icons. I can't seem to stringify it to make a match. I tried pushing to an array and toString(). CodeSandbox
✈ ["✈", "♘", "✈", "♫", "♫", "☆", "♘", "☆"]
Here is the code snippet (Parent)
removeMatches(icon) {
const item = icon;
const iconsArray = this.props.cardTypes;
const newIconsArray =iconsArray.filter(function(item) {
item !== icon
})
this.setState({ cardTypes: newIconsArray });
}
This is a function in the parent component Cards, when the child component is clicked I pass a value into an onClick. Below is a click handler in the Child component
handleVis(e) {
const item = e.target.value
this.props.removeMatches(item)
}
First of all, there's nothing really different about filtering an "icon" string array from any other strings. Your example works like this:
const icons = ["✈", "♘", "✈", "♫", "♫", "☆", "♘", "☆"]
const icon = "✈";
const filteredIcons = icons.filter(i => i !== icon);
filteredIcons // ["♘", "♫", "♫", "☆", "♘", "☆"]
Your CodeSandbox example has some other issues, though:
Your Card.js component invokes this.props.removeMatches([item]) but the removeMatches function treats the argument like a single item, not an array.
Your Cards.js removeMatches() function filters this.props.cardTypes (with the previously mentioned error about treating the argument as a single item not an array) but does not assign the result to anything. Array.filter() returns a new array, it does not modify the original array.
Your Cards.js is rendering <Card> components from props.cardTypes, this means that Cards.js is only rendering the cards from the props it is given, so it cannot filter that prop from inside the component. You have a few options:
Pass the removeMatches higher up to where the cards are stored in state, in Game.js as this.state.currentCards, and filter it in Game.js which will pass the filtered currentCards back down to Cards.js.
// Game.js
removeMatches = (items) => {
this.setState(prevState => ({
currentCards: prevState.currentCards.filter(card => items.indexOf(card) == -1)
}));
}
// ...
<Cards cardTypes={this.state.currentCards} removeMatches={this.removeMatches} />
// Cards.js
<Card removeMatches={this.props.removeMatches}/>
// Card.js -- same as it is now
Move Cards.js props.cardTypes into state (ex state.currentCards) within Cards.js, then you can filter it out in Cards.js and render from state.currentCards instead of props.cardTypes. To do this you would also need to hook into componentWillReceiveProps() to make sure that when the currentCards are passed in as prop.cardTypes from Game.js that you update state.currentCards in Cards.js. That kind of keeping state in sync with props can get messy and hard to follow, so option 1 is probably better.
// Cards.js
state = { currentCards: [] }
componentWillReceiveProps(nextProps) {
if (this.props.cardTypes !== nextProps.cardTypes) {
this.setState({ currentCards: nextProps.cardTypes });
}
}
removeMatches = (items) => {
this.setState(prevState => ({
currentCards: prevState.currentCards.filter(card => items.indexOf(card) == -1)
}));
}
render() {
return (
<div>
{ this.state.currentCards.map(card => {
// return rendered card
}) }
</div>
);
}
Store all the removed cards in state in Cards.js and filter cardTypes against removedCards before you render them (you will also need to reset removedCards from componentWillReceiveProps whenever the current cards are changed):
// Cards.js
state = { removedCards: [] }
componentWillReceiveProps(nextProps) {
if (this.props.cardTypes !== nextProps.cardTypes) {
this.setState({ removedCards: [] });
}
}
removeMatches = (items) => {
this.setState(prevState => ({
removedCards: [...prevState.removedCards, ...items]
}));
}
render() {
const remainingCards = this.props.cardTypes.filter(card => {
return this.state.removedCards.indexOf(card) < 0;
});
return (
<div>
{ remainingCards.map(card => {
// return rendered card
})}
</div>
);
}
As you can see, keeping state in one place in Game.js is probably your cleanest solution.
You can see all 3 examples in this forked CodeSandbox (the second 2 solutions are commented out): https://codesandbox.io/s/6yo42623p3