export default function SearchPage() {
const [searchString, setSearchString] = React.useState("");
const [apiCall, setApiCall] = React.useState<() => Promise<Collection>>();
const {isIdle, isLoading, isError, error, data} = useApi(apiCall);
const api = useContext(ApiContext);
useEffect(()=>console.log("APICall changed to", apiCall), [apiCall]);
const doSearch = (event: React.FormEvent) => {
event.preventDefault();
setApiCall(() => () => api.search(searchString));
};
const doNext = () => {
var next = api.next;
if (next) {
setApiCall(()=>(() => next)());
}
window.scrollTo(0, 0);
}
const doPrev = () => {
if (api.prev) {
setApiCall(() => api.prev);
}
window.scrollTo(0, 0);
}
return (
<>
<form className={"searchBoxContainer"} onSubmit={doSearch}>
<TextField
label={"Search"}
variant={"filled"}
value={searchString}
onChange={handleChange}
className={"searchBox"}
InputProps={{
endAdornment: (
<IconButton onClick={() => setSearchString("")}>
<ClearIcon/>
</IconButton>
)
}}
/>
<Button type={"submit"} variant={"contained"} className={"searchButton"}>Go</Button>
</form>
{
(isIdle) ? (
<span/>
) : isLoading ? (
<span>Loading...</span>
) : isError ? (
<span>Error: {error}</span>
) : (
<Paper className={"searchResultsContainer"}>
<Box className={"navButtonContainer"}>
<Button variant={"contained"}
disabled={!api.prev}
onClick={doPrev}
className={"navButton"}>
{"< Prev"}
</Button>
<Button variant={"contained"}
disabled={!api.next}
onClick={doNext}
className={"navButton"}>
{"Next >"}
</Button>
</Box>
<Box className={"searchResults"}>
{
data && data.items().all().map(item => (
<span className={"thumbnailWrapper"}>
<img className={"thumbnail"}
src={item.link("preview")?.href}
alt={(Array.from(item.allData())[0].object as SearchResponseDataModel).title}/>
</span>
))
}
</Box>
<Box className={"navButtonContainer"}>
<Button variant={"contained"}
disabled={!api.prev}
onClick={doPrev}
className={"navButton"}>
{"< Prev"}
</Button>
<Button variant={"contained"}
disabled={!api.next}
onClick={doNext}
className={"navButton"}>
{"Next >"}
</Button>
</Box>
</Paper>
)
}
</>
)
}
For various reasons, I've got a function stored in my state (it's for use with the react-query library). I'm seeing very odd behaviour when I try and update it, though. When any of doSearch, doNext, or doPrev are called, it successfully updates the state - the useEffect hook is firing properly and I can see the message in console - but it's not triggering a re-render until the window loses and regains focus.
Most of the other people I've seen with this problem have been storing an array in their state, and updating the array rather than creating a new one - so the hooks don't treat it as a new object, and the re-render doesn't happen. I'm not using an array, though, I'm using a function, and passing it different function objects. I'm absolutely stumped and have no idea what's going on.
EDIT: It seems it might not be the rendering failing to fire, but the query hook not noticing that its input has changed? I've edited the code above to show the whole function, and my custom hook is below.
function useApi(func?: () => Promise<Collection>) {
return useQuery(
["doApiCall", func],
func || (async () => await undefined),
{
enabled: !!func,
keepPreviousData: true
}
)
}
You can’t put a function into the queryKey. Keys need to be serializable. See: https://react-query.tanstack.com/guides/query-keys#array-keys
Related
I have a list of items, each of which is represented by a component and supports a couple of actions such as "Like", "Save", I have 2 options: one is to keep the handlers in list(Parent) component and pass the handlers to each item component or I can have the handlers in the item component, the code may look like below, I am wondering which one is better, Thanks!
Solution 1:
const ItemComp = ({item, onLike, onSave}) => {
return (
<>
<p>{item.name}</p>
<div>
<button onClick={() => onLike(item.id)}>Like</button>
<button onClick={() => onSave(item.id)}>Save</button>
</div>
</>
)
}
const ListComp = ({items}) => {
const handleLike = (id) => {
console.log(id)
// like it
}
const handleSave = (id) => {
console.log(id)
// save it
}
return (
{items.map(
(item) => {
return <ItemComp item={item} onLike={handleLike} onSave={handleSave} >
}
)}
)
}
<List items={items} />
Solution 2:
const ItemComp = ({item}) => {
// keep event handlers inside of child component
const handleLike = (id) => {
console.log(id)
// like it
}
const handleSave = (id) => {
console.log(id)
// save it
}
return (
<>
<p>{item.name}</p>
<div>
<button onClick={() => handleLike(item.id)}>Like</button>
<button onClick={() => handleSave(item.id)}>Save</button>
</div>
</>
)
}
const ListComp = ({items}) => {
return (
{items.map(
(item) => {
return <ItemComp item={item} >
}
)}
)
}
<List items={items} />
if you are going to use the component in the same context throughout the whole app, or write an explanatory document for your components, its better if the handlers are inside. Otherwise, you would have to write new handlers for each time you use the component.
Think of it as a UI Library. Pass the data, see the result :)
BUT,
if you are going to use the component as a general, container kind of component, you'll have to write the handlers outside, because the component's own handlers wouldn't know how to handle the different kind of data if you introduce new contexts for it.
Im using react hooks to create a multi-select component, that generates sliders for each selection (using material-ui). Once a user makes their selections I'm generating an individual slider for each selection using map. Each slider component has its own instance of useState to keep track of the value of the slider. Im passing a nodeid as a key to the component and would like to have an array of [nodeid, valueFromSlider] for each slider returned. For example: if a user makes 3 selections and then calls 'update feedback scores' button, returned values should be [[nodeid, valueFromSlider], [nodeid, valueFromSlider], [nodeid, valueFromSlider]].
What is the best way for my parent component to know what each individual sliders state is? is there a way to call component by id? Should i be usings forms or something? first time really using hooks since switching from redux.
Here is my code, thanks in advance.
const NodeSlider = (props) => {
const { node, nodeId } = props;
const [value, setValue] = useState(5);
const handleChange = (event, newValue) => {
setValue(newValue);
};
return (
<div>
<Typography id="discrete-slider" gutterBottom>
{node.display_name}
</Typography>
<Slider
step={1}
marks
min={-10}
max={10}
valueLabelDisplay="on"
defaultValue={5}
value={value}
onChange={handleChange}
/>
</div>
);
}
const TunerSlider = (props) => {
const { sliders } = props;
const [feedbackArray, setFeedbackArray] = useState(null);
console.log('sliders', sliders);
const createFeedbackArray = () => {
// FIGURE THIS OUT? use this to call setfeedback array with value from node slider?
};
return (
<div>
{sliders.map((slider) => (
<NodeSlider
key={slider.node_id}
node={slider}
nodeId={slider.node_id}
/>
))}
<Button
variant="contained"
color="primary"
onClick={() => createFeedbackArray()}
>
Update Feedback Scores
</Button>
</div>
);
};
const TunerSearch = ({ nodeData }) => {
const [searchValues, setSearchValues] = useState(null);
const [sliderValues, setSliderValues] = useState(null);
const generateSlider = () => {
setSliderValues(searchValues);
};
return (
<div>
<div>Tuner Search</div>
<Autocomplete
multiple
id="tags-outlined"
options={nodeData}
getOptionLabel={(option) => option.display_name}
filterSelectedOptions
onChange={(event, value) => setSearchValues(value)}
renderInput={(params) => (
<TextField
{...params}
variant="outlined"
label="select nodes"
placeholder="add more nodes"
/>
)}
/>
<Button
variant="contained"
color="primary"
onClick={() => generateSlider()}
>
Generate Node Sliders
</Button>
{sliderValues ? <TunerSlider sliders={sliderValues} /> : null}
</div>
);
};
export default TunerSearch;
The best approach would be to move the states to the highest common parent.
Then you can define your function that will track your feedback in the parent component and then share them with the children component(s).
Here is an example of how I would approach this:
I have a simple component that copies a link to the clipboard, and would like to swap the link icon with a checkmark. I have the logic setup to do so, but having an issue getting the state reset after 3 seconds to reset the button back to the link icon. How can I properly setup my useEffect and state hook to set and then reset the state for showing/hiding the link to checkmark and back again?
const [copySuccess, setCopySuccess] = useState('');
const [visible, setVisible] = useState(true);
const copyToClipBoard = async copyHeader => {
try {
await navigator.clipboard.writeText(copyHeader);
setCopySuccess('Copied!');
} catch (err) {
setCopySuccess('Failed to copy!');
}
};
<Button>
{copySuccess ? (
<Icon name="success" />
):(
<Icon
name="linked"
onClick={() => copyToClipBoard(url)}
/>
)}
</Button>
I was trying a useEffect like so:
useEffect(() => {
setTimeout(() => {
setVisible(false);
}, 3000);
});
but not sure how to use the setVisible state and timeout, to swap the icon back to the link to let users know they can copy it again.
You can derive the visible state from copySuccess state, try adding it to useEffect dep array:
const [copySuccess, setCopySuccess] = useState("");
const copyToClipBoard = async (copyHeader) => {
try {
await navigator.clipboard.writeText(copyHeader);
setCopySuccess("Copied!");
} catch (err) {
setCopySuccess("Failed to copy!");
}
};
useEffect(() => {
if (copySuccess !== "") {
setTimeout(() => {
setCopySuccess("");
}, 3000);
}
}, [copySuccess]);
<Button>
{copySuccess ? (
<Icon name="success" />
) : (
<Icon name="linked" onClick={() => copyToClipBoard(url)} />
)}
</Button>;
See similar logic in codesandbox example:
function Component() {
const [copyIsAvailable, setCopyIsAvailable] = useState(true);
useEffect(() => {
setTimeout(() => {
setCopyIsAvailable(true);
}, 1000);
}, [copyIsAvailable]);
return (
<button onClick={() => setCopyIsAvailable(false)}>
{copyIsAvailable ? "copy" : "copied"}
</button>
);
}
I could suggest you changing the async function to update visible.
Then change thee button tag:
<Button>
{visible
? <Icon name="success" />
: <Icon
name="linked"
onClick={() => copyToClipBoard(url)}
/>
}
</Button>
I am practicing a todo list project using React 16 Hooks. However, I found it's hard to get the index using inside map() function like this:
Parent Todo Component:
const Todo = () => {
const dispatch = useDispatch();
const { list } = useSelector(state => state.todoReducer.toJS());
const [value, setValue] = useState('');
function handleOnchange (e) {
setValue(e.target.value)
}
function handleAddItem() {
actionCreators.addItem(dispatch, value);
setValue('');
}
function handleRemoveItem(index) {
// if use handleChecked(index) will trigger handleItemChecked and printed all //indexes everytime
actionCreators.removeItem(dispatch, value);
}
function handleItemChecked(index) {
console.log(index)
}
return (
<>
<input type="text" value={value} onChange={handleOnchange} />
<button onClick={handleAddItem}>+</button>
<List items={list} handleClick={handleRemoveItem} isCompeted={false} handleChecked={handleItemChecked}/>
</>
)
}
Child Component:
const List = ({items, handleClick, isCompleted, handleChecked}) => {
return (
<ListWrapper>
{items && items.length > 0 && items.map((item, index) => {
return (
<ListWrapper.item key={`${item}${new Date()}${Math.random()}`}>
{/* if like this: onClick={handleChecked(index)} will cause the issue */}
{/* <li>{item}<input type="checkbox" onClick={handleChecked(index)}/></li> */}
<li>{item}<input type="checkbox" name={index} onClick={e => handleChecked(e.target.name)}/></li>
<button onClick={handleClick}>-</button>
</ListWrapper.item>
);
})}
</ListWrapper>
)
}
I found if in the child component: List, if I need to get the index of item, I have to assign name={index} . If using handleChecked(index) directly, will cause rendering many times issue in its parent component(Todo). Is any better way to handle this case? Thank you so much in advanced!
as commented by jonrsharpe:
<button onClick={handleClick}>-</button>
here's 2 known methods to fix it:
<button onClick={() => handleClick(index)}>-</button>
or
<button onClick={handleClick.bind(this, index)}>-</button>
read about that: https://reactjs.org/docs/handling-events.html
hope helped :)
I have a nav menu built with material-ui/core in Navbar.
I use useRef to track the position of clicked button on toggle menu close.
anchorRef.current.contains(event.target)
And I am getting 'Uncaught TypeError: anchorRef.current.contains is not a function' .
I tried 'Object.values(anchorRef.current).includes(event.target)' instead, it always returns false.
-- update --
anchorRef.current.props Object.
withStyles {
props:{
aria-haspopup: "true"
aria-owns: undefined
children: "계정"
className: "nav-menu--btn"
onClic: f onClick()
get ref: f()
isReactWarning: true
arguments: (...)
caller: (...)
length: 0
name: "warnAboutAccessingRef"
...
}, context{...}, refs{...}, ...}
ToggleMenuList
const ToggleMenuList = ({ navAdminList, navAdminItems, classes }) => {
const [activeId, setActiveId] = useState(null);
const anchorRef = useRef(null);
const handleToggle = id => {
setActiveId(id);
};
const handleClose = event => {
if (anchorRef.current && anchorRef.current.contains(event.target)) {
return;
}
setActiveId(null);
};
return (
<React.Fragment>
<div className={`nav-menu--admin ${classes.root}`}>
{navAdminList.map(e => (
<div key={e.id}>
<Button
ref={anchorRef}
aria-owns={activeId === e.id ? 'menu-list-grow' : undefined}
aria-haspopup="true"
onClick={() => handleToggle(e.id)}
>
{e.name}
</Button>
{activeId === e.id && (
<ToggleMenuItems
id={e.id}
activeId={activeId}
handleClose={handleClose}
anchorRef={anchorRef}
items={navAdminItems[e.id]}
/>
)}
</div>
))}
</div>
</React.Fragment>
);
};
export default withStyles(styles)(ToggleMenuList);
ToggleMenuItems
const ToggleMenuItems = ({
listId,
activeId,
handleClose,
anchorRef,
items,
}) => {
const isOpen = activeId === listId;
const leftSideMenu = activeId === 3 || activeId === 4 ? 'leftSideMenu' : '';
return (
<Popper
open={isOpen}
anchorEl={anchorRef.current}
keepMounted
transition
disablePortal
>
{({ TransitionProps, placement }) => (
<Grow
{...TransitionProps}
style={{
transformOrigin:
placement === 'bottom' ? 'center top' : 'center bottom',
}}
className={`toggle-menu ${leftSideMenu}`}
>
<Paper id="menu-list-grow">
<ClickAwayListener
onClickAway={handleClose}
>
<MenuList className="toggle-menu--list">
{items.map(e => (
<MenuItem
key={e.id}
className="toggle-menu--item"
onClick={handleClose}
>
<Link
to={e.to}
className="anchor td-none c-text1 toggle-menu--link"
>
{e.name}
</Link>
</MenuItem>
))}
</MenuList>
</ClickAwayListener>
</Paper>
</Grow>
)}
</Popper>
);
};
export default ToggleMenuItems;
react: ^16.8.6
react-dom: ^16.8.6
react-router-dom: ^4.3.1
#material-ui/core: ^3.1.2
I assume your ToggleMenuItems sets up global(document-level?) event listener on click to collapse Menu on clicking somewhere outside.
And you have a sibling button element. Clicking on that you want to keep menu expanded, right? So that was the point to use .contains in onClick to check if we are clicked outside of ToggleMenuItems but in scope of specific Button. The reason why it does not work: <Button> is custom class-based React component so it returns React component instance in ref. And it does not have any DOM-specific methods like .contains
You can rework you current approach: just stop bubbling event in case Button has been clicked. It would stop global event handler set by ToggleMenuItems to react.
const stopPropagation = (event) => event.stopPropagation();
const ToggleMenuList = ({ navAdminList, navAdminItems, classes }) => {
const [activeId, setActiveId] = useState(null);
const anchorRef = useRef(null);
const handleToggle = id => {
setActiveId(id);
};
const handleClose = event => {
setActiveId(null);
};
return (
<React.Fragment>
<div className={`nav-menu--admin ${classes.root}`}>
{navAdminList.map(e => (
<div key={e.id}>
<div onClick={stopPropagation}>
<Button
aria-owns={activeId === e.id ? 'menu-list-grow' : undefined}
aria-haspopup="true"
onClick={() => handleToggle(e.id)}
>
{e.name}
</Button>
</div>
{activeId === e.id && (
<ToggleMenuItems
id={e.id}
activeId={activeId}
handleClose={handleClose}
anchorRef={anchorRef}
items={navAdminItems[e.id]}
/>
)}
</div>
))}
</div>
</React.Fragment>
);
};
export default withStyles(styles)(ToggleMenuList);
I've put stopPropagation handler outside since it does not depend on any internal variable.