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>
);
};
Related
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 have this Component:
export const DashboardPage = () => {
const [mounted, setMounted] = useState(false);
const [currentNumber, setCurrentNumber] = useState(null);
const [insertedNumber, setInsertedNumber] = useState(null);
const [result, setResult] = useState(null);
const [arrayNumbers, setArrayNumbers] = useState([]);
useEffect(() => {
const rnd = randomNumber();
if (!mounted) {
setCurrentNumber(rnd);
setMounted(true);
}
}, [mounted]);
const handleChange = (e) => {
setInsertedNumber(e.target.value);
}
const handleClick = (e) => {
e.preventDefault();
arrayNumbers.push(insertedNumber);
setArrayNumbers(arrayNumbers);
setResult("Oops, ritenta! era " + currentNumber)
if (parseInt(insertedNumber) === parseInt(currentNumber)) {
setResult("EVVIVA hai vinto!");
}
}
const handleOnSubmit = (e) => {
e.preventDefault();
}
return (
<>
<h2>Storico tentativi: {arrayNumbers.map(e => e)}</h2>
<h2>La soluzione è: {currentNumber}</h2>
<Form onSubmit={handleOnSubmit}>
<Form.Group controlId="formGTN">
<Form.Label>Guess the number:</Form.Label>
<Form.Control type="text" onChange={handleChange} />
</Form.Group>
<Button variant="primary" type="submit" onClick={handleClick} label="Is it?" />
</Form>
<h1>{result}</h1>
</>
)
}
export default DashboardPage;
It is a simple "Guess The Number" play.
I have the issue that:
On very first loading of page, I can get into arrayNumbers the only one number I inserted.
From second and other attempts, when I CHANGE the value inside the input, so when I delete previous number, I get state update and I have the previous (and on) numbers. Like if "state" is "back" of one try.
I can see this behavious also in Chrome's Redux plugin. State is update ONLY a step other.
I could move the push into array on onChange, but... If I insert a number of 2 digits I get 2 insert on array (44 will be "4" and "4").
THank you!
You should never mutate the state and/or update the state without setState. In JavaScript, the Array.push() method mutates the original array. So, in your example, you mutate arrayNumbers but you should add a new element to the array without updating the original one.
Also, listening to the onSubmit event is enough, you don't have to add the onClick event to your submit button. You can move everything from handleClick to handleOnSubmit.
export const DashboardPage = () => {
const [mounted, setMounted] = useState(false);
const [currentNumber, setCurrentNumber] = useState(null);
const [insertedNumber, setInsertedNumber] = useState(null);
const [result, setResult] = useState(null);
const [arrayNumbers, setArrayNumbers] = useState([]);
useEffect(() => {
const rnd = randomNumber();
if (!mounted) {
setCurrentNumber(rnd);
setMounted(true);
}
}, [mounted]);
const handleChange = (e) => {
setInsertedNumber(e.target.value);
};
const handleOnSubmit = (e) => {
e.preventDefault();
setArrayNumbers([...arrayNumbers, insertedNumber]);
if (parseInt(insertedNumber, 10) === parseInt(currentNumber, 10)) {
setResult('EVVIVA hai vinto!');
} else {
setResult('Oops, ritenta! era ' + currentNumber);
}
};
return (
<>
<h2>Storico tentativi: {arrayNumbers.map((e) => e)}</h2>
<h2>La soluzione è: {currentNumber}</h2>
<Form onSubmit={handleOnSubmit}>
<Form.Group controlId="formGTN">
<Form.Label>Guess the number:</Form.Label>
<Form.Control type="text" onChange={handleChange} />
</Form.Group>
<Button variant="primary" type="submit" label="Is it?" />
</Form>
<h1>{result}</h1>
</>
);
};
For the above component, you might try these changes in the handleClick function:
const handleClick = (e) => {
e.preventDefault();
let newArr = [...arrayNumbers];
newArr.push(insertedNumber);
setArrayNumbers(newArr);
setResult('Oops, ritenta! era ' + currentNumber);
if (parseInt(insertedNumber) === parseInt(currentNumber)) {
setResult('EVVIVA hai vinto!');
}
};
Im doing a onclick that changes the individual numbers colors, its working fine in useState(the colors change):
const [totalNumbers, setTotalNumbers]: any = useState([1, 2, 3]);
But when i push to useState: totalNumbers.push(event.target.id), only the array is increased but the colors didnt render
const newBet: React.FC = () => {
const [range, setRange] = useState(gamesJson[whichLoteriaIsVar].range);
const numbersList = Array.from(Array(25).keys()).map((num) => num + 1);
const [totalNumbers, setTotalNumbers]: any = useState([]);
const changeButtonColor = (event: any) => {
totalNumbers.push(Number(event.target.id));
console.log(totalNumbers);
setTotalNumbers(totalNumbers);
};
return(
<NumbersContainer>
{numbersList.map((num) => (
<Numbers
onClick={changeButtonColor}
id={num.toString()}
className={
totalNumbers.indexOf(num) === -1 ? 'desactive' : 'active'
}
key={num}
>
{formatNumber(num)}
</Numbers>
))}
</NumbersContainer>
)
}
Try this:
const changeButtonColor = (event: any) => {
const newtotalNumbers = totalNumbers.slice()
newtotalNumbers.push(Number(event.target.id));
setSelectedNumbers(newtotalNumbers );
};
const changeButtonColor = (event: any) => {
totalNumbers.push(Number(event.target.id));
console.log(totalNumbers);
setTotalNumbers([...totalNumbers]);
};
Solved my problem
I am just learning about Apollo-React but I couldn't make graphql request
This is how I do without Apollo
const Search = () => {
const [searchedText, setSearchedText] = React.useState('')
const [suggestions, setSuggestions] = React.useState([])
const [selected, setSelected] = React.useState(null)
const debounceHandler = (searchedText) => debounce(() => {
sendQuery(`{search(str:"${searchedText}") {name}}`).then(({search}) => {
if (!search) return
setSuggestions(search)
})
}, 500)
const handleInputChange = async (e) => {
if(e.key === 'Enter') {
const name = e.target.value
sendQuery(`{getPokemon(str:"${name}"){name, image}}`).then(({getPokemon}) => {
setSelected(getPokemon)
})
}
debounceHandler(searchedText)()
}
return (
<div>
<h1>Pokemon Search</h1>
<input type="text" value={searchedText} onChange={(e) => setSearchedText(e.target.value)} onKeyUp={(e) => handleInputChange(e)} style={{width:'100%'}} />
<hr />
<div>
{selected ? <PokemonProfile selected={selected} /> : suggestions.map(({name}) => (
<ShowSuggestion name={name} searchedText={searchedText} setSelected={setSelected}/>
)) }
</div>
</div>
)
}
Now without my own sendQuery function, I want to use Apollo's useQuery hook.
const GET_POKEMON = gql`
query getPokemon ($str: String!) {
getPokemon(str: $str) {
name
image
}
}
`;
const SEARCH = gql `
query search($str: String!) {
search(str:$str) {
name
}
}
`;
These are my queries and results correctly on the playground. Now I write Search function again. I say whenever searchedText changes (WHen user types in), query Search and set the returning data as suggestions. Whenever user hits enter, I want to query the Pokemon from backend and set it as selected.
const Search = () => {
const [searchedText, setSearchedText] = React.useState(null)
const [suggestions, setSuggestions] = React.useState([])
const [selected, setSelected] = React.useState(null)
React.useEffect(() => {
const { data } = useQuery(SEARCH, {
variables: { "str": searchedText },
pollInterval: 500,
});
if (data) {
setSuggestions(data)
}
}, [searchedText])
const fetchAndSelect = name => {
setSearchedText('')
const { pokemon } = useQuery(GET_POKEMON, {
variables: {
"str": name
}
})
setSelected(pokemon)
}
const handleInputChange = (e) => {
const name = e.target.value
if(e.key === 'Enter') {
return fetchAndSelect(name)
}
setSearchedText(name)
}
return (
<div>
<h1>Pokemon Search</h1>
<input type="text" value={searchedText} onKeyUp={(e) => handleInputChange(e)} style={{width:'100%'}} />
<hr />
<div>
{selected ? <PokemonProfile selected={selected} /> : suggestions.map(({name}) => (
<ShowSuggestion name={name} searchedText={searchedText} setSelected={setSelected}/>
))}
</div>
</div>
)
}
But this gives Invalid hook call error. If I don't make the query inside useEffect ( I am not sure what is wrong with this?) this time I get Rendered more hooks than during the previous render. error. I am not sure what I am doing wrong?
EDIT
Based on answer I edit the code like following
const Search = () => {
const [searchedText, setSearchedText] = React.useState(null)
const [suggestions, setSuggestions] = React.useState([])
const [selected, setSelected] = React.useState(null)
const debouncedSearch = debounce(searchedText, 1000) // Trying to debounce the searched text
const [searchPokemons, { data }] = useLazyQuery(SEARCH);
const [getPokemon, { pokemon }] = useLazyQuery(GET_POKEMON)
React.useEffect(() => {
if (!searchedText) return
setSelected(null)
searchPokemons({ variables: { str: searchedText }})
if (data) {
console.log(data)
setSuggestions(data)
}
}, [debouncedSearch])
const fetchAndSelect = name => {
setSearchedText('')
getPokemon({variables: {str: name}})
if (pokemon) {
setSelected(pokemon)
}
}
const handleInputChange = (e) => {
const name = e.target.value
if(e.key === 'Enter') {
return fetchAndSelect(name)
}
setSearchedText(name)
}
return (
<div>
<h1>Pokemon Search</h1>
<input type="text" value={searchedText} onKeyUp={(e) => handleInputChange(e)} style={{width:'100%'}} />
<hr />
<div>
{selected ? <PokemonProfile selected={selected} /> : suggestions.map(({name}) => (
<ShowSuggestion name={name} searchedText={searchedText} setSelected={setSelected}/>
))}
</div>
</div>
)
}
I am unable to type anything on the input. It is fetching like crazy. Please help
You should use useLazyQuery Hook in this case. It is very useful for things that happen at an unknown point in time, such as in response to a user's search operation.
How about If you call use your hook on the top of your function and just call it inside the useEffect hook.
const [search, { data }] = useLazyQuery(SEARCH, {
variables: { "str": searchedText },
pollInterval: 500,
});
React.useEffect(() => {
if (searchedText)
search() // Function for executing the query
if (data)
setSuggestions(data)
}, [searchedText])
As you see, useLazyQuery handles fetching data in a synchronous way without any promises.
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>
)
}