What's the best solution to get access to the child's state using react-hooks?
I've tried various approaches. Below the one that I've ended up with.
Form (Parent)
export const Form: FunctionComponent<IProps> = ({ onFinish, initState }) => {
const formInputsStateRef = useRef({})
const handleFinish = () => {
const params = formInputsStateRef.current
console.log(params)
onFinish(params)
}
return (
<div>
<Inputs initState={initState} stateRef={formInputsStateRef} />
<S.Button onClick={handleFinish}>
Finish
</S.Button>
</div>
)
}
Inputs (Child)
export const Inputs: FunctionComponent<IProps> = ({ initState, stateRef }) => {
const [pool, setPool] = useState(initState.pool)
const [solarPanel, setSolarPanel] = useState(initState.solarPanel)
useEffect(() => {
stateRef.current = { pool, solarPanel }
})
const handlePoolInput = () => {
setPool('new pool')
}
const handleSolarPanelInput = () => {
setSolarPanel('new solar panel')
}
return (
<div>
<h2>{pool}</h2>
<S.Button onClick={handlePoolInput}>Change pool</S.Button>
<h2>{solarPanel}</h2>
<S.Button onClick={handleSolarPanelInput}>Change solar panel</S.Button>
<h2>-----</h2>
</div>
)
}
It works that way but I don't like the fact that it creates an object on every render.
Inputs(Child)
useEffect(() => {
stateRef.current = { pool, solarPanel }
})
You could pass pool and solarPanel as the second argument to useEffect so that the state is updated to ref only on these values change
export const Inputs: FunctionComponent<IProps> = ({ initState, stateRef }) => {
const [pool, setPool] = useState(initState.pool)
const [solarPanel, setSolarPanel] = useState(initState.solarPanel)
useEffect(() => {
stateRef.current = { pool, solarPanel }
}, [pool, solarPanel])
const handlePoolInput = () => {
setPool('new pool')
}
const handleSolarPanelInput = () => {
setSolarPanel('new solar panel')
}
return (
<div>
<h2>{pool}</h2>
<S.Button onClick={handlePoolInput}>Change pool</S.Button>
<h2>{solarPanel}</h2>
<S.Button onClick={handleSolarPanelInput}>Change solar panel</S.Button>
<h2>-----</h2>
</div>
)
}
However to have a more controlled handle of child values using ref, you can make use of useImperativeHandle hook.
Child
const InputsChild: FunctionComponent<IProps> = ({ initState, ref }) => {
const [pool, setPool] = useState(initState.pool)
const [solarPanel, setSolarPanel] = useState(initState.solarPanel)
useImperativeHandle(ref, () => ({
pool,
solarPanel
}), [pool, solarPanel])
const handlePoolInput = () => {
setPool('new pool')
}
const handleSolarPanelInput = () => {
setSolarPanel('new solar panel')
}
return (
<div>
<h2>{pool}</h2>
<S.Button onClick={handlePoolInput}>Change pool</S.Button>
<h2>{solarPanel}</h2>
<S.Button onClick={handleSolarPanelInput}>Change solar panel</S.Button>
<h2>-----</h2>
</div>
)
}
export const Inputs = forwardRef(InputsChild);
Parent
export const Form: FunctionComponent<IProps> = ({ onFinish, initState }) => {
const formInputsStateRef = useRef({})
const handleFinish = () => {
const params = formInputsStateRef.current
console.log(params)
onFinish(params)
}
return (
<div>
<Inputs initState={initState} ref={formInputsStateRef} />
<S.Button onClick={handleFinish}>
Finish
</S.Button>
</div>
)
}
Related
I have fetch method in useEffect hook:
export const CardDetails = () => {
const [ card, getCardDetails ] = useState();
const { id } = useParams();
useEffect(() => {
fetch(`http://localhost:3001/cards/${id}`)
.then((res) => res.json())
.then((data) => getCardDetails(data))
}, [id])
return (
<DetailsRow data={card} />
)
}
But then inside DetailsRow component this data is not defined, which means that I render this component before data is fetched. How to solve it properly?
Just don't render it when the data is undefined:
export const CardDetails = () => {
const [card, setCard] = useState();
const { id } = useParams();
useEffect(() => {
fetch(`http://localhost:3001/cards/${id}`)
.then((res) => res.json())
.then((data) => setCard(data));
}, [id]);
if (card === undefined) {
return <>Still loading...</>;
}
return <DetailsRow data={card} />;
};
There are 3 ways to not render component if there aren't any data yet.
{data && <Component data={data} />}
Check if(!data) { return null } before render. This method will prevent All component render until there aren't any data.
Use some <Loading /> component and ternar operator inside JSX. In this case you will be able to render all another parts of component which are not needed data -> {data ? <Component data={data} /> : <Loading>}
If you want to display some default data for user instead of a loading spinner while waiting for server data. Here is a code of a react hook which can fetch data before redering.
import { useEffect, useState } from "react"
var receivedData: any = null
type Listener = (state: boolean, data: any) => void
export type Fetcher = () => Promise<any>
type TopFetch = [
loadingStatus: boolean,
data: any,
]
type AddListener = (cb: Listener) => number
type RemoveListener = (id: number) => void
interface ReturnFromTopFetch {
addListener: AddListener,
removeListener: RemoveListener
}
type StartTopFetch = (fetcher: Fetcher) => ReturnFromTopFetch
export const startTopFetch = function (fetcher: Fetcher) {
let receivedData: any = null
let listener: Listener[] = []
function addListener(cb: Listener): number {
if (receivedData) {
cb(false, receivedData)
return 0
}
else {
listener.push(cb)
console.log("listenre:", listener)
return listener.length - 1
}
}
function removeListener(id: number) {
console.log("before remove listener: ", id)
if (id && id >= 0 && id < listener.length) {
listener.splice(id, 1)
}
}
let res = fetcher()
if (typeof res.then === "undefined") {
receivedData = res
}
else {
fetcher().then(
(data: any) => {
receivedData = data
},
).finally(() => {
listener.forEach((cb) => cb(false, receivedData))
})
}
return { addListener, removeListener }
} as StartTopFetch
export const useTopFetch = (listener: ReturnFromTopFetch): TopFetch => {
const [loadingStatus, setLoadingStatus] = useState(true)
useEffect(() => {
const id = listener.addListener((v: boolean, data: any) => {
setLoadingStatus(v)
receivedData = data
})
console.log("add listener")
return () => listener.removeListener(id)
}, [listener])
return [loadingStatus, receivedData]
}
This is what myself needed and couldn't find some simple library so I took some time to code one. it works great and here is a demo:
import { startTopFetch, useTopFetch } from "./topFetch";
// a fakeFetch
const fakeFetch = async () => {
const p = new Promise<object>((resolve, reject) => {
setTimeout(() => {
resolve({ value: "Data from the server" })
}, 1000)
})
return p
}
//Usage: call startTopFetch before your component function and pass a callback function, callback function type: ()=>Promise<any>
const myTopFetch = startTopFetch(fakeFetch)
export const Demo = () => {
const defaultData = { value: "Default Data" }
//In your component , call useTopFetch and pass the return value from startTopFetch.
const [isloading, dataFromServer] = useTopFetch(myTopFetch)
return <>
{isloading ? (
<div>{defaultData.value}</div>
) : (
<div>{dataFromServer.value}</div>
)}
</>
}
Try this:
export const CardDetails = () => {
const [card, setCard] = useState();
const { id } = useParams();
useEffect(() => {
if (!data) {
fetch(`http://localhost:3001/cards/${id}`)
.then((res) => res.json())
.then((data) => setCard(data))
}
}, [id, data]);
return (
<div>
{data && <DetailsRow data={card} />}
{!data && <p>loading...</p>}
</div>
);
};
Here's the scenario. I have a app and a search component.
const App = () => {
const [search, setSearch] = useState("initial query");
// ... other code
return (
<Search search={search} setSearch={setSearch} />
...other components
);
};
const Search = ({ search, setSearch }) => {
const [localSearch, setLocalSearch] = useState(search);
const debouncedSetSearch = useMemo(() => debounce(setSearch, 200), [setSearch]);
const handleTextChange = useCallback((e) => {
setLocalSearch(e.target.value);
debouncedSetSearch(e.target.value);
}, [setLocalSearch]);
return (
<input value={localSearch} onChange={handleTextChange} />
);
}
It's all good until this point. But I want to know what's the best way to change the search text on an external event. So far, the best approach I've found is using events.
const App = () => {
const [search, setSearch] = useState("initial query");
// ... other code
useEffect(() => {
onSomeExternalEvent((newSearch) => {
setSearch(newSearch);
EventBus.emit("updateSearch", newSearch);
});
}, []);
return (
<Search search={search} setSearch={setSearch} />
...other components
);
};
const Search = ({ search, setSearch }) => {
const [localSearch, setLocalSearch] = useState(search);
const debouncedSetSearch = useMemo(() => debounce(setSearch, 200), [setSearch]);
const handleTextChange = useCallback((e) => {
setLocalSearch(e.target.value);
debouncedSetSearch(e.target.value);
}, [setLocalSearch]);
useEffect(() => {
EventBus.subscribe("updateSearch", (newSearch) => {
setLocalSearch(newSearch);
});
}, []);
return (
<input value={localSearch} onChange={handleTextChange} />
);
}
Is there a better (correct) way of doing this?
I'm trying to learn typescript, currently creating a note taking app. It's very simple: when you click on adding a new note, you a get an empty textarea, where you can edit your note. I'm able to add notes, but I can't update the value of each textarea. What am I doing wrong?
Here's what I have so far:
const [showSidePanel, setShowSidePanel] = React.useState<boolean>(false);
const [notes, setNotes] = React.useState([{text: '', id: nanoid()}]);
const [noteText, setNoteText] = React.useState<string>('');
const addNote = (): void => {
const newNote = {text: 'hey', id: nanoid()};
setNotes([...notes, newNote])
}
const handleChange = (event: ChangeEvent<HTMLTextAreaElement>) => {
setNoteText(event.target.value)
}
const toggleSidePanel = React.useCallback(() => setShowSidePanel(!showSidePanel), [showSidePanel]);
const wrapperRef = React.useRef<HTMLDivElement>(null);
useClickOutside(wrapperRef, () => setShowSidePanel(false));
return (
<div ref={wrapperRef}>
<GlobalStyle />
<SidePanel showSidePanel={showSidePanel}>
<Button onClick={addNote}>Add note</Button>
{notes.map((n) =>
<Note onChange={() => handleChange} text={noteText} key={n.id}/>
)}
</SidePanel>
<ToggleButton onClick={toggleSidePanel}>Open</ToggleButton>
</div>
);
}
If I understand it correctly, each Note is a text-area and you want to update each one of them independently and noteText state is used for active text-area component, correct?
If that's the case then we can either (1)remove noteText state and update notes array directly for appropriate notes.id or (2)we can preserve noteText state and update the notes array after debouncing.
Latter solution is preferable and coded below:
// a hook for debouncing
export const useDebouncedValue = (value, timeOut=500) => {
const [debouncedValue, setDebouncedValue] = useState(null);
useEffect(() => {
let someTimeout;
someTimeout = setTimeout(() => {
setDebouncedValue(value);
}, timeOut);
return () => clearInterval(someTimeout);
}, [value, timeOut]);
return {
debouncedValue,
};
};
Main logic for updating the notes array logic
const Comp = () => {
const [showSidePanel, setShowSidePanel] = React.useState(false);
const [notes, setNotes] = React.useState([{ text: "", id: nanoid() }]);
const [currNoteText, setCurrNoteText] = React.useState({
text: "",
id: "",
});
// this always holds the debounced value
const updatedNoteText = useDebouncedValue(currNoteText.text, 600);
// effect will get triggered whenever the currNoteText.text is changed and pause of 600ms is taken
useEffect(() => {
const notesIndex = notes.findIndex((ele) => ele.id === currNoteText.id);
if (notesIndex >= 0) {
const updatedNotes = _.cloneDeep(notes);
updatedNotes[notesIndex].text = updatedNoteText;
// updation of notes array
setNotes(updatedNotes);
}
}, [updatedNoteText]);
const addNote = () => {
const newNote = { text: "hey", id: nanoid() };
setNotes([...notes, newNote]);
};
const handleChange = (event, noteId) => {
// setting current changed note in currNoteText Object
setCurrNoteText({ id: noteId, text: event.target.value });
};
const toggleSidePanel = React.useCallback(
() => setShowSidePanel(!showSidePanel),
[showSidePanel]
);
const wrapperRef = React.useRef(null);
useClickOutside(wrapperRef, () => setShowSidePanel(false));
return (
<div ref={wrapperRef}>
<GlobalStyle />
<SidePanel showSidePanel={showSidePanel}>
<Button onClick={addNote}>Add note</Button>
{notes.map((n) => (
<Note
// here along with event we are also passing note-id
onChange={(e) => handleChange(e, n.id)}
text={noteText}
key={n.id}
/>
))}
</SidePanel>
<ToggleButton onClick={toggleSidePanel}>Open</ToggleButton>
</div>
);
};
I'm trying to display a new, filtered array that hides the rest of the elements and leaves only the ones I type in the search bar. The const newFilter works in the console but doesn't read in the return. I tried placing the const in other places but it's beyond the scope..
import React, { useState } from "react";
function SearchBar({ placeholder }) {
const [filteredData, setFilteredData] = useState([]);
const [wordEntered, setWordEntered] = useState("");
const [pokemonData, setPokemonData] = React.useState({});
React.useEffect(() => {
fetch(
"https://raw.githubusercontent.com/Biuni/PokemonGO-Pokedex/master/pokedex.json"
)
.then((res) => res.json())
.then((data) => setPokemonData(data.pokemon));
}, []);
const allPokes = pokemonData;
const pokemons = Object.values(allPokes);
const handleFilter = (event) => {
const searchWord = event.target.value;
setWordEntered(searchWord);
const newFilter = pokemons.filter((value) => {
return value.name.toLowerCase().includes(searchWord.toLowerCase());
});
if (searchWord === "") {
setFilteredData([]);
} else {
setFilteredData(newFilter);
}
console.log(newFilter);
};
let checkConsole = () => alert("Check the console :)");
return (
<div className="search-div">
<p className="search-text">Name or Number</p>
<div className="search">
<div className="searchInputs">
<input
type="text"
placeholder={placeholder}
value={wordEntered}
onChange={handleFilter}
/>
</div>
</div>
</div>
);
}
export default SearchBar;
In the given snippet, there is no filtered data displaying logic inside the return
import React, { useState } from "react";
export default function SearchBar({ placeholder }) {
const [filteredData, setFilteredData] = useState([]);
const [wordEntered, setWordEntered] = useState("");
const [pokemonData, setPokemonData] = React.useState({});
React.useEffect(() => {
fetch(
"https://raw.githubusercontent.com/Biuni/PokemonGO-Pokedex/master/pokedex.json"
)
.then((res) => res.json())
.then((data) => setPokemonData(data.pokemon));
}, []);
React.useEffect(() => {
console.log(filteredData);
});
const allPokes = pokemonData;
const pokemons = Object.values(allPokes);
const handleFilter = (event) => {
const searchWord = event.target.value;
setWordEntered(searchWord);
const newFilter = pokemons.filter((value) => {
return value.name.toLowerCase().includes(searchWord.toLowerCase());
});
if (searchWord === "") {
setFilteredData([]);
} else {
setFilteredData(newFilter);
}
console.log(newFilter);
};
let checkConsole = () => alert("Check the console :)");
return (
<div className="search-div">
<p className="search-text">Name or Number</p>
<div className="search">
<div className="searchInputs">
<input
type="text"
placeholder={placeholder}
value={wordEntered}
onChange={handleFilter}
/>
</div>
/* Add filteredData logic in the return */
{filteredData.map((each) => (
<p>{each.name}</p>
))}
</div>
</div>
);
}
Here is my problem I want to call function refresh from MyTable.js into MyPage.js
MyTable.js
const MyTable = (props) => {
//Some state
const refresh = () => {//Do Refresh}
return(<Table/>);
}
export default MyTable;
MyPage.js
const MyPage = (props) => {
//Some state
const handleRefresh = () => {
//How to call refresh of MyTable here?
}
return(
<Button onClick={handleRefresh} >Refresh</Button>
<MyTable />
)
}
//I Want a solution something like this but I don't know how to achieve
const MyPage = (props) => {
//Some state
const [myTable] = MyTable.useMyTable();
const handleRefresh = () => {
myTable.refresh();
}
return(
<Button onClick={handleRefresh} >Refresh</Button>
<MyTable table={myTable} />
)
}
There is a way to do it but not highly recommended.
You can do it like the following
MyTable.js
const MyTable = (props) => {
props.refresh(() => {
// DO Refresh
});
return(<Table/>);
}
export default MyTable;
MyPage.js
const MyPage = (props) => {
//Some state
let refreshFunc = () => {};
const handleRefresh = () => {
//How to call refresh of MyTable here?
refreshFunc();
}
return(
<Button onClick={handleRefresh} >Refresh</Button>
<MyTable refresh={r => { refreshFunc = r; }} />
)
}
Try to patch as follows:
const MyPage = (props) => {
//Some state
const [refresh, setRefresh] = useState(0);
const handleRefresh = () => {
setRefresh(x => x + 1);
}
return(
<Button onClick={handleRefresh} >Refresh</Button>
<MyTable dummy={refresh} />
)
}