I'm using the YTS API and I need to change the link for the call, I have to use
?query_term= and add the text that the user is typing, for autocomplete. I'm using mantine components for the autocomplete. I tried putting the call inside the handlechange function, but this is not possible.
const [movieNames, setMovieNames] = useState([])
const onChangeHandler = (text) => {
useEffect(() => {
const loadMovieNames = async () => {
const response = await axios.get('https://yts.mx/api/v2/list_movies.json?query_term='+text);
let arrayOfMoviesNames = [];
response.data.data.movies.forEach(i => {
arrayOfMoviesNames.push(i.title)
});
setMovieNames(arrayOfMoviesNames)
}
loadMovieNames()
}, [])
}
.
<Autocomplete
placeholder="Search Movie"
limit={8}
data={movieNames}
onChange={e => onChangeHandler(e.target.value)}
/>
You MUST use hooks in the execution context of Function Component, you used the useEffect inside a function not in the execution context of Function Component.
const YourComponent = () => {
const [movieNames, setMovieNames] = useState([]);
const loadMovieNames = async (text) => {
const response = await axios.get(
'https://yts.mx/api/v2/list_movies.json?query_term=' + text
);
let arrayOfMoviesNames = [];
response.data.data.movies.forEach((i) => {
arrayOfMoviesNames.push(i.title);
});
setMovieNames(arrayOfMoviesNames);
};
return (
<Autocomplete
placeholder="Search Movie"
limit={8}
data={movieNames}
onChange={(value) => loadMovieNames(value)}
/>
);
};
It is also possible without useEffect, so without making it so complicated by using useEffect and onChangeHandler both, only use onChangeHandler function to update the movieNames and it will automatically update the DOM texts (I mean where ever you use)...
import React, { useState } from "react";
function MoviesPage(props) {
const [ movieNames, setMovieNames ] = useState([]);
const [ searchValue, setSearchValue ] = useState("");
const onChangeHandler = async (text) => {
const response = await axios.get(
'https://yts.mx/api/v2/list_movies.json?query_term=' + text
);
let arrayOfMoviesNames = [];
response.data.data.movies.forEach(i => {
arrayOfMoviesNames.push(i.title)
});
setMovieNames(arrayOfMoviesNames);
}
return (
<div>
<Autocomplete
placeholder="Search Movie"
limit={8}
data={movieNames}
onChange={(e) => onChangeHandler(e.target.value)}
/>
</div>
);
}
export default MoviesPage;
...and just to clarify, you can use useEffect in case of API if you want to initialize the page with the API data. You can use this hook if you don't have any onChange handlers. Another way you can approach is you can update a state hook (like searchData) on the change of the Search Bar, and lastly add the the searchData variable to the useEffect dependency array:
useEffect(() => {
// use the searchData variable to populate or update the page
// ...
},
[
searchData, // <-- talking about this line
]);
So, this was my solution. Hope this helps you mate!
useEffect is a hook, which executes on state change, So keep the useEffect funtion outside the onChangeHandler and add a new state for 'query param' and setQueryState(text) inside the onChangeHandler, and put the state param as dependency in useEffect, So whenever this state gets changed this will call the use effect function automatically.
Related
I have a controlled component that I call Note. I want its default value to be equal to the selected note (which is set in App.js and passed through as a prop). It seems redundant/bad practice. Here's my code, simplified to the relevant parts. How can I set the default value of textarea to be equal to another state variable?
Edit: Forgot to mention that selectedNote is changed in another component. It works for the state set in useEffect but not for the updates.
App.js
function App(){
const [selectedNote, setSelectedNote] = useState("")
useEffect(() => {
async function fetchData(){
let req = await fetch("http://localhost:9292/notes");
let res = await req.json();
setSelectedNote(res[0])
}
fetchData()
},[])
return (
<Note selectedNote={selectedNote.body}/>
)
}
Note.js
function Note({selectedNote}) {
const [editValue, setEditValue] = useState(selectedNote)
return (
<form>
<textarea value={editValue} onChange={handleChange}>
</textarea>
</form>
)
}
(To clarify, I have no issues if I write const [editValue, setEditValue] = useState("testing123") or some other string)
So ideally you want to lift state up so that the parent component manages the state updates, and the Notes component is as dumb as possible.
In this example the data is loaded into state, and then the notes are built, only receiving an id, some body text which will be their value, and an onChange handler.
When the text is changed, the state is copied, the object in the array (defined by the id) updated, and the new array pushed back into state.
const { useEffect, useState } = React;
const json = '[{"id":1,"body":"Note1"},{"id":2,"body":"Note2"},{"id":3,"body":"Note3"}]';
function mockApi() {
return new Promise(res => {
setTimeout(() => res(json), 2000);
});
}
function Example() {
const [ notes, setNotes ] = useState([]);
const [ selectedNote, setSelectedNote ] = useState(null);
useEffect(() => {
mockApi()
.then(res => JSON.parse(res))
.then(data => setNotes(data));
}, []);
function handleChange(e) {
const { value, dataset: { id } } = e.target;
const copy = [...notes];
copy[id - 1].body = value;
setNotes(copy);
}
function handleClick() {
console.log(JSON.stringify(notes));
}
if (!notes.length) return 'Loading';
return (
<div>
{notes.map(note => {
return (
<Note
key={note.id}
id={note.id}
body={note.body}
handleChange={handleChange}
/>
)
})}
<button onClick={handleClick}>
View state
</button>
</div>
);
}
function Note({ id, body, handleChange }) {
return (
<textarea
data-id={id}
value={body}
onChange={handleChange}
/>
);
}
ReactDOM.render(
<Example />,
document.getElementById('react')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.production.min.js"></script>
<div id="react"></div>
You can provide a function to useState that will only be invoked once, when the component renders. Use that function to copy the prop value into the Note's private state.
const [editValue, setEditValue] = useState(() => selectedNote)
You may have other problems, such as the prop value being blank on initial render, but this is still usually the most straightforward way to initialize a private state var based on a prop.
If it turns out that blank-initial-state is an insurmountable problem, then you may instead need to set up a useEffect that updates the private state when the prop value changes to a satisfactory value. Something like this:
const [editValue, setEditValue] = useState()
React.useEffect(() => {
// only update state if old is blank & new is not
if(!editValue && selectedNote) setEditValue(selectedNote)
}, [selectedNote])
I'm having problems with setting up lodash debounce in the function to make an API request. For some reason callback doesn't happen and the value sends every time I type.
import debounce from "lodash/debounce";
const handleChange = (event) => {
const { value } = event.target;
const debouncedSave = debounce((nextValue) => dispatch(movieActions.getMovies(nextValue), 1000));
debouncedSave(value);
};
I'm using material ui and have this in return:
<Autocomplete
onInputChange={handleChange}
/>
Your debounced function is created multiple times for each change event and that causes the problem. I will use a simplified example with a simple input and a console.log instead of your dispatch, but you can apply the solution to your case as well.
The simplest solution would be to move the debouncedSave declaration outside your component.
const debouncedSave = debounce((nextValue) => console.log(nextValue), 1000);
export default function App() {
const handleChange = (e) => {
const { value } = e.target;
debouncedSave(value);
};
return <input onChange={handleChange} />;
}
or else if you want to keep the debounced function declaration inside your component you can use a ref, to create and use the same instance each time, no matter the re-renders:
export default function App() {
const debouncedSaveRef = useRef(
debounce((nextValue) => console.log(nextValue), 1000)
);
const handleChange = (e) => {
const { value } = e.target;
debouncedSaveRef.current(value);
};
return <input onChange={handleChange} />;
}
I wrote a program that takes and displays contacts from an array, and we have an input for searching between contacts, which we type and display the result.
I used if in the search function to check if the searchKeyword changes, remember to do the filter else, it did not change, return contacts and no filter is done
I want to do this control with useEffect and I commented on the part I wrote with useEffect. Please help me to reach the solution of using useEffect. Thank you.
In fact, I want to use useEffect instead of if
I put my code in the link below
https://codesandbox.io/s/simple-child-parent-comp-forked-4qf39?file=/src/App.js:905-913
Issue
In the useEffect hook in your sandbox you aren't actually updating any state.
useEffect(()=>{
const handleFilterContact = () => {
return contacts.filter((contact) =>
contact.fullName.toLowerCase().includes(searchKeyword.toLowerCase())
);
};
return () => contacts;
},[searchKeyword]);
You are returning a value from the useEffect hook which is interpreted by React to be a hook cleanup function.
See Cleaning up an effect
Solution
Add state to MainContent to hold filtered contacts array. Pass the filtered state to the Contact component. You can use the same handleFilterContact function to compute the filtered state.
const MainContent = ({ contacts }) => {
const [searchKeyword, setSearchKeyword] = useState("");
const [filtered, setFiltered] = useState(contacts.slice());
const setValueSearch = (e) => setSearchKeyword(e.target.value);
useEffect(() => {
const handleFilterContact = () => {
if (searchKeyword.length >= 1) {
return contacts.filter((contact) =>
contact.fullName.toLowerCase().includes(searchKeyword.toLowerCase())
);
} else {
return contacts;
}
};
setFiltered(handleFilterContact());
}, [contacts, searchKeyword]);
return (
<div>
<input
placeholder="Enter a keyword to search"
onChange={setValueSearch}
/>
<Contact contacts={contacts} filter={filtered} />
</div>
);
};
Suggestion
I would recommend against storing a filtered contacts array in state since it is easily derived from the passed contacts prop and the local searchKeyword state. You can filter inline.
const MainContent = ({ contacts }) => {
const [searchKeyword, setSearchKeyword] = useState("");
const setValueSearch = (e) => setSearchKeyword(e.target.value);
const filterContact = (contact) => {
if (searchKeyword.length >= 1) {
return contact.fullName
.toLowerCase()
.includes(searchKeyword.toLowerCase());
}
return true;
};
return (
<div>
<input
placeholder="Enter a keyword to search"
onChange={setValueSearch}
/>
<Contact contacts={contacts.filter(filterContact)} />
</div>
);
};
import React, { useState, useEffect } from "react";
import axios from "axios";
const App = () => {
let [countries, setCountries] = useState([]);
const [newCountry, newStuff] = useState("");
const hook = () => {
//console.log("effect");
axios.get("https://restcountries.eu/rest/v2/all").then((response) => {
console.log("promise fulfilled");
setCountries(response.data);
//console.log(response.data);
});
};
const filter = (event) => {
newStuff(event.target.value);
if (event.target.value === undefined) {
return
} else {
let value = event.target.value;
console.log(value);
countries = countries.filter((country) => country.name.startsWith(value));
setCountries(countries);
console.log(countries);
}
};
useEffect(hook, []);
return (
<div>
<p>find countries</p>
<input value={newCountry} onChange={filter} />
<ul>
{countries.map((country) => (
<li key={country.name.length}>{country.name}</li>
))}
</ul>
</div>
);
};
export default App;
So I have a search bar so that when you enter a few characters it will update the state and show the countries that start with the respective first characters. However, nothing is being shown when I enter input into my search bar. Also, my filter function, when I console.log my countries array which is supposed to have the countries that start with the characters I entered, it's always an empty array.
You need some changes in order to make this work:
Use two states for countries, one for the list you
get in the initial render and another for the current filter
countries.
const [countriesStore, setCountriesStore] = useState([]); // this only change in the first render
const [countries, setCountries] = useState([]); // use this to print the list
I recomed to use any tool to manage the state and create a model for
the countries ther you can make the side effect there and create an
action that update the countries store. I'm using Easy Peasy in
my current project and it goes very well.
Take care of the filter method because startsWith
method is not case-insensitive. You need a regular expression or
turn the current country value to lower case. I recommend to use
includes method to match seconds names like island in the search.
const filterCountries = countriesStore.filter(country => {
return country.name.toLowerCase().includes(value);
});
Remove the if condition in the filter in order to include the
delete action in the search and get the full list again if
everything is removed.
Just in the case, empty the search string state in the first
render
useEffect(() => {
hook();
setSearchString("");
}, []);
Replace the length in the list key. You can use the name and trim to remove space.
<li key={country.name.trim()}>{country.name}</li>
The final code look like this:
export default function App() {
const [countriesStore, setCountriesStore] = useState([]);
const [countries, setCountries] = useState([]);
const [searchString, setSearchString] = useState("");
const hook = () => {
axios.get("https://restcountries.eu/rest/v2/all").then(response => {
console.log("promise fulfilled");
setCountriesStore(response.data);
setCountries(response.data);
});
};
const filter = event => {
setSearchString(event.target.value);
let value = event.target.value;
const filterCountries = countriesStore.filter(country => {
return country.name.toLowerCase().includes(value);
});
setCountries(filterCountries);
};
useEffect(() => {
hook();
setSearchString("");
}, []);
return (
<div>
<p>find countries</p>
<input value={searchString} onChange={filter} />
<ul>
{countries.map(country => (
<li key={country.name.trim()}>{country.name}</li>
))}
</ul>
</div>
);
}
You need to wrap your hook into async useCallback:
const hook = useCallback(async () => {
const {data} = await axios.get("https://restcountries.eu/rest/v2/all");
setCountries(data);
}, []);
you are not able to mutate state countries. Use immutable way to update your state:
const filter = (event) => {
newStuff(event.target.value);
if (event.target.value === undefined) {
return
} else {
let value = event.target.value;
setCountries(countries.filter((country) => country.name.startsWith(value)));
}
};
And useState is asynchronous function. You will not see result immediately. Just try to console.log outside of any function.
I'm trying to implement a data stream that has to use inner observables, where I use one from mergeMap, concatMap etc.
e.g.:
const output$$ = input$$.pipe(
mergeMap(str => of(str).pipe(delay(10))),
share()
);
output$$.subscribe(console.log);
This works fine when logging into console.
But when I try to use it in React like below utilizing useEffect and useState hooks to update some text:
function App() {
const input$ = new Subject<string>();
const input$$ = input$.pipe(share());
const output$$ = input$$.pipe(
mergeMap(str => of(str).pipe(delay(10))),
share()
);
output$$.subscribe(console.log);
// This works
const [input, setInput] = useState("");
const [output, setOutput] = useState("");
useEffect(() => {
const subscription = input$$.subscribe(setInput);
return () => {
subscription.unsubscribe();
};
}, [input$$]);
useEffect(() => {
const subscription = output$$.subscribe(setOutput);
// This doesn't
return () => {
subscription.unsubscribe();
};
}, [output$$]);
return (
<div className="App">
<input
onChange={event => input$.next(event.target.value)}
value={input}
/>
<p>{output}</p>
</div>
);
}
it starts acting weird/unpredictable (e.g.: sometimes the text is updated in the middle of typing, sometimes it doesn't update at all).
Things I have noticed:
If the inner observable completes immediately/is a promise that
resolves immediately, it works fine.
If we print to console instead of useEffect, it works fine.
I believe this has to do something with the inner workings of useEffect and how it captures and notices outside changes, but cannot get it working.
Any help is much appreciated.
Minimal reproduction of the case:
https://codesandbox.io/s/hooks-and-observables-1-7ygd8
I'm not quite sure what you're trying to achieve, but I found a number of problems which hopefully the following code fixes:
function App() {
// Create these observables only once.
const [input$] = useState(() => new Subject<string>());
const [input$$] = useState(() => input$.pipe(share()));
const [output$$] = useState(() => input$$.pipe(
mergeMap(str => of(str).pipe(delay(10))),
share()
));
const [input, setInput] = useState("");
const [output, setOutput] = useState("");
// Create the subscription to input$$ on component mount, not on every render.
useEffect(() => {
const subscription = input$$.subscribe(setInput);
return () => {
subscription.unsubscribe();
};
}, []);
// Create the subscription to output$$ on component mount, not on every render.
useEffect(() => {
const subscription = output$$.subscribe(setOutput);
return () => {
subscription.unsubscribe();
};
}, []);
return (
<div className="App">
<input
onChange={event => input$.next(event.target.value)}
value={input}
/>
<p>{output}</p>
</div>
);
}
I had a similar task but the goal was to pipe and debounce the input test and execute ajax call.
The simple answer that you should init RxJS subject with arrow function in the react hook 'useState' in order to init subject once per init.
Then you should useEffect with empty array [] in order to create a pipe once on component init.
import React, { useEffect, useState } from "react";
import { ajax } from "rxjs/ajax";
import { debounceTime, delay, takeUntil } from "rxjs/operators";
import { Subject } from "rxjs/internal/Subject";
const App = () => {
const [items, setItems] = useState([]);
const [loading, setLoading] = useState(true);
const [filterChangedSubject] = useState(() => {
// Arrow function is used to init Singleton Subject. (in a scope of a current component)
return new Subject<string>();
});
useEffect(() => {
// Effect that will be initialized once on a react component init.
// Define your pipe here.
const subscription = filterChangedSubject
.pipe(debounceTime(200))
.subscribe((filter) => {
if (!filter) {
setLoading(false);
setItems([]);
return;
}
ajax(`https://swapi.dev/api/people?search=${filter}`)
.pipe(
// current running ajax is canceled on filter change.
takeUntil(filterChangedSubject)
)
.subscribe(
(results) => {
// Set items will cause render:
setItems(results.response.results);
},
() => {
setLoading(false);
},
() => {
setLoading(false);
}
);
});
return () => {
// On Component destroy. notify takeUntil to unsubscribe from current running ajax request
filterChangedSubject.next("");
// unsubscribe filter change listener
subscription.unsubscribe();
};
}, []);
const onFilterChange = (e) => {
// Notify subject about the filter change
filterChangedSubject.next(e.target.value);
};
return (
<div>
Cards
{loading && <div>Loading...</div>}
<input onChange={onFilterChange}></input>
{items && items.map((item, index) => <div key={index}>{item.name}</div>)}
</div>
);
};
export default App;