How do I add a click handler to refetch data from my API based on my input ON CLICK?
In my console I'm getting back data if I input "Jon Snow" for instance because the onChange set to e.target.value but not sure how to fetch this on button click.
Code Sandbox: https://codesandbox.io/s/pedantic-lichterman-4ev6f?file=/src/game.jsx
import React, { useEffect, useState } from "react";
import axios from "axios";
export default function Game() {
const [error, setError] = useState(null);
const [name, setName] = useState("");
const handleSubmit = e => {
e.preventDefault();
console.log( name );
}
const handleClick = e => {
// ??
}
useEffect(() => {
fetch(`https://anapioficeandfire.com/api/characters?name=${name}`)
.then((res) => res.json())
.then((data) => {
console.log(data[0].name); // the data I want back
})
.catch((error) => {
console.log("Error", error);
setError(error);
});
}, [name]);
return (
<form onSubmit={handleSubmit}>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Name"
/>
<input type="submit" value="Submit" onClick={handleClick}/>
</form>
);
}
When the Submit button is clicked it will trigger onSubmit event, no need for you to handle the onClick event separately.
import React, { useEffect, useState } from "react";
import axios from "axios";
export default function Game() {
const [error, setError] = useState(null);
const [name, setName] = useState("");
const handleSubmit = e => {
e.preventDefault();
console.log( name );
fetchData(name);
}
const fetchData = (name) => {
fetch(`https://anapioficeandfire.com/api/characters?name=${name}`)
.then((res) => res.json())
.then((data) => {
console.log(data[0].name); // the data I want back
})
.catch((error) => {
console.log("Error", error);
setError(error);
});
}
useEffect(() => {
fetchData(name);
}, []);
return (
<form onSubmit={handleSubmit}>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Name"
/>
<input type="submit" value="Submit" onClick={handleClick}/>
</form>
);
}
Add another stateful variable. You need not only a value and setter for the input value but also a value and setter for the API results you want to be able to use elsewhere. Maybe something like
const [searchText, setSearchText] = useState('');
const [result, setResult] = useState('');
// inside fetch callback:
setResult(data[0]?.name ?? ''); // use optional chaining to not throw an error
// if there is no result
<input
type="text"
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
placeholder="Name"
/>
And then you can use the result where you need.
Live demo:
const App = () => {
const [error, setError] = React.useState(null);
const [searchText, setSearchText] = React.useState('');
const [result, setResult] = React.useState('');
const handleSubmit = e => {
e.preventDefault();
console.log( name );
}
React.useEffect(() => {
fetch(`https://anapioficeandfire.com/api/characters?name=${searchText}`)
.then((res) => res.json())
.then((data) => {
setResult(data[0] ? data[0].name : '');
})
.catch((error) => {
console.log("Error", error);
setError(error);
});
}, [searchText]);
console.log(result);
return (
<form onSubmit={handleSubmit}>
<input
type="text"
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
placeholder="Name"
/>
<input type="submit" value="Submit" onClick={e => e.preventDefault()}/>
</form>
);
}
ReactDOM.render(<App />, document.querySelector('.react'));
<script crossorigin src="https://unpkg.com/react#16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#16/umd/react-dom.development.js"></script>
<div class='react'></div>
You can use direct state variable [name] in handleClick function.
The other answers are all correct that you should trigger the fetch in your handleSubmit. I just wanted to chime in with some sample code for rendering results since you asked for help with that.
The API returns an array of characters. We want to map through that result and show each character. We also want to tell the user if there were no results (especially since this API seems to only work with an exact name and will return a result for "Arya Stark" but not for "Stark"). We don't want to show that "No Characters Found" message before they have submitted.
I am using a setState hook to store the array of character matches from the API. I am initializing the state to undefined instead of [] so that we only show the no results message if it gets set to [].
My code allows the user to submit multiple times. We keep displaying the previous results until they submit a new search. Once we have an array in our characters state, we display those results.
// an example component to render a result
const RenderCharacter = ({ name, aliases }) => {
return (
<div>
<h2>{name}</h2>
{aliases.length && (
<div>
<h3>Aliases</h3>
<ul>
{aliases.map((a) => (
<li key={a}>{a}</li>
))}
</ul>
</div>
)}
</div>
);
};
export default function Game() {
// current form input
const [name, setName] = useState("");
// save characters returned from the API
// start with undefined instead of empty array
// so we know when to show "no characters found" message
const [characters, setCharacters] = useState();
// store API errors
const [error, setError] = useState(null);
const fetchData = () => {
fetch(`https://anapioficeandfire.com/api/characters?name=${name}`)
.then((res) => res.json())
.then(setCharacters) // store data to state
.then(() => setError(null)) // clear previous errors
.catch((error) => {
console.log("Error", error);
setError(error);
setCharacters(undefined); // clear previous character matches
});
};
const handleSubmit = (e) => {
e.preventDefault();
fetchData();
};
return (
<div>
<form onSubmit={handleSubmit}>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Name"
/>
<input type="submit" value="Submit" />
</form>
{characters !== undefined &&
(characters.length === 0 ? (
<div>No Characters Found</div>
) : (
<div>
{characters.map((character) => (
<RenderCharacter key={character.name} {...character} />
))}
</div>
))}
{error !== null && <div>Error: {error.message}</div>}
</div>
);
}
Code Sandbox Demo (with typescript annotations)
Related
I want to get Movie list(API) when I submit,
so I made my API link by using Template literals like this ▼
const getMovies = async () => {
const json = await (
await fetch(
`https://yts.mx/api/v2/list_movies.json?minimum_rating=8&page=${Math.round(Math.random()*100)}&query_term=${movieName}&sort_by=year`
)
).json();
setMovies(json.data.movies);
setLoading(false);
};
then, I filled 'movieName' into the array of useEffect. because I want to Refetch the API everytime the 'movieName' Changed.
but! it dosen't work:(
what is the problem?
▼ code i wrote
import { useEffect, useRef, useState } from "react";
import Movie from "../components/Movie";
function Home(){
const [loading, setLoading] = useState(true);
const [movies, setMovies] = useState([]);
const [movieSearch, setMovieSearch] = useState('');
const [movieName, setMovieName] = useState('');
const getMovies = async () => {
const json = await (
await fetch(
`https://yts.mx/api/v2/list_movies.json?minimum_rating=8&page=${Math.round(Math.random()*100)}&query_term=${movieName}&sort_by=year`
)
).json();
setMovies(json.data.movies);
setLoading(false);
};
const onChange = (event) =>{
setMovieSearch(event.target.value)
}
const onSubmit = (event)=>{
event.preventDefault();
setMovieName(movieSearch)
}
useEffect(() => {
getMovies();
}, [movieName]);
return (
<>
<h4>Search</h4>
<form onSubmit={onSubmit}>
<input
onChange={onChange}
type="text"
value={movieSearch}
placeholder="..."
></input>
</form>
{loading ? (
<h3>Loading</h3>
) : (
<div>
{movies.map((item) => (
<Movie
key={item.id}
id={item.id}
title={item.title}
year={item.year}
medium_cover_image={item.medium_cover_image}
rating={item.rating}
runtime={item.runtime}
genres={item.genres}
summary={item.summary}
/>
))}
</div>
)}
</>
);
}
why not trying to move getMovies() that is inside the useEffect to the onSubmit() function right after you set the movie name?
Because you don't update the answerName state on the onChange event of the search input
const onChange = (event) =>{
const value = event.target.value
setMovieSearch(value)
setMovieName(value)
}
So basically I'm trying to create a code that allows me to update the slug with the use of params.
Don't know why My code throws this error.
"TypeError: Cannot read property 'params' of undefined in react".
I tried replacing
useEffect(() => {
loadCategory();
}, []);
with
useEffect(() => {
if(match.params.slug) loadOrders()
}, [match.params.slug])
but it still didn't work.
This is the code I wrote.
import React, { useState, useEffect } from "react";
import {
HistoryContainer,
HistoryBg,
TextContainer2,
TextContainer3,
Text,
CatForm,
FormLabel,
FormControl,
ButtonPrimary,
} from "./CategoryUpdateElements";
import AdminNav from "../AdminNav/index";
import { toast } from "react-toastify";
import { useSelector } from "react-redux";
import { getCategory, updateCategory } from "../../../functions/category";
const CategoryUpdate = ({ history, match }) => {
const { user } = useSelector((state) => ({ ...state }));
const [name, setName] = useState("");
const [loading, setLoading] = useState(false);
useEffect(() => {
loadCategory();
}, []);
const loadCategory = () =>
getCategory(match.params.slug).then((c) => setName(c.data.name));
const handleSubmit = (e) => {
e.preventDefault();
// console.log(name);
setLoading(true);
updateCategory(match.params.slug, { name }, user.token)
.then((res) => {
// console.log(res)
setLoading(false);
setName("");
toast.success(`"${res.data.name}" is updated`);
history.push("/admin/category");
})
.catch((err) => {
console.log(err);
setLoading(false);
if (err.response.status === 400) toast.error(err.response.data);
});
};
return (
<>
<HistoryContainer>
<HistoryBg>
<AdminNav />
<TextContainer2>
<TextContainer3>
{loading ? <Text>Loading..</Text> : <Text>Update category</Text>}
<CatForm onSubmit={handleSubmit}>
<FormLabel>Name</FormLabel>
<FormControl
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
autoFocus
required
/>
<ButtonPrimary>Save</ButtonPrimary>
</CatForm>
</TextContainer3>
</TextContainer2>
</HistoryBg>
</HistoryContainer>
</>
);
};
export default CategoryUpdate;
UPDATE:
To add context to this problem. This code lets me update the name of the slug, but the TypeError doesn't let me follow through with this haha. I was actually following a tutorial regarding this and obviously, his code works. I was sure that I was following it properly as I wrote the code exactly like his but the only difference is my ui.
I also tried console logging match and after checking it out, what I saw was "undefined" which is not surprising.. It should have shown me the slug but instead it gave me "undefined".
This is his code which allows him to update his slug.
import React, { useState, useEffect } from "react";
import AdminNav from "../../../components/nav/AdminNav";
import { toast } from "react-toastify";
import { useSelector } from "react-redux";
import { getCategory, updateCategory } from "../../../functions/category";
const CategoryUpdate = ({ history, match }) => {
const { user } = useSelector((state) => ({ ...state }));
const [name, setName] = useState("");
const [loading, setLoading] = useState(false);
useEffect(() => {
loadCategory();
}, []);
const loadCategory = () =>
getCategory(match.params.slug).then((c) => setName(c.data.name));
const handleSubmit = (e) => {
e.preventDefault();
// console.log(name);
setLoading(true);
updateCategory(match.params.slug, { name }, user.token)
.then((res) => {
// console.log(res)
setLoading(false);
setName("");
toast.success(`"${res.data.name}" is updated`);
history.push("/admin/category");
})
.catch((err) => {
console.log(err);
setLoading(false);
if (err.response.status === 400) toast.error(err.response.data);
});
};
const categoryForm = () => (
<form onSubmit={handleSubmit}>
<div className="form-group">
<label>Name</label>
<input
type="text"
className="form-control"
onChange={(e) => setName(e.target.value)}
value={name}
autoFocus
required
/>
<br />
<button className="btn btn-outline-primary">Save</button>
</div>
</form>
);
return (
<div className="container-fluid">
<div className="row">
<div className="col-md-2">
<AdminNav />
</div>
<div className="col">
{loading ? (
<h4 className="text-danger">Loading..</h4>
) : (
<h4>Update category</h4>
)}
{categoryForm()}
<hr />
</div>
</div>
</div>
);
};
export default CategoryUpdate;
Still new to coding. Hope you guys can help me with this ^_^
I think your problem with match which is getting as the props. If you are having trouble with handle match props please try
useRouteMatch instaed.
import { useRouteMatch } from "react-router-dom";
function YourComponent() {
let match = useRouteMatch();
// Do whatever you want with the match...
return <div />;
}
I think this is more convinent to use.
For more examples
I was trying to set my value in the input value! but after that, I cannot write anything in the input field! I wanted to set values from the back end in value!
We are writing an admin channel to edit the article for that we need already existing article values to edit the article! What am I doing wrong! or Maybe you can suggest a better way to edit the article in the admin channel!
here is the code:
import React, { useState, useEffect } from 'react';
import axios from 'axios';
import { useParams } from 'react-router';
const EditArticle = (props) => {
const [editValues, setEditValues] = useState([]);
const [changedValues, setChangedValues] = useState('');
console.log('values', editValues);
console.log('changed', changedValues);
const params = useParams();
console.log(params);
const resultsId = params.id;
console.log('string', resultsId);
const [authTokens, setAuthTokens] = useState(
localStorage.getItem('token') || ''
);
const setTokens = (data) => {
localStorage.setItem('token', JSON.stringify(data));
setAuthTokens(data);
// setToken(data['dataValues']['token']);
};
useEffect(() => {
const fetchData = async () => {
try {
const res = await axios.get(
`${process.env.REACT_APP_API_URL}/article/${resultsId}`
);
setEditValues(res.data);
} catch (err) {}
};
fetchData();
}, [resultsId]);
const inputValue = editValues;
const userToken = props.token;
return (
<div>
<form value={{ authTokens, setAuthTokens: setTokens }}>
<input
value={editValues.title || ''}
onChange={(input) => setChangedValues(input.target.value)}
type='text'
/>
<input
// ref={editValues.shortDesc}
value={editValues.shortDesc}
onChange={(input) => setChangedValues(input.target.value)}
type='text'
/>
<button type='submit'>send</button>
</form>
</div>
);
};
export default EditArticle;
your onChange handler is updating a different state property than what is being used as the value on the input (editValues vs changedValues).
Also you can pass a defaultValue to input that will get used as the default value only.
See more here https://reactjs.org/docs/uncontrolled-components.html
you can use just do it just using editValues. try this:
I just reproduced it without the api call to run the code.
import React, { useState, useEffect } from "react";
const EditArticle = (props) => {
const [editValues, setEditValues] = useState([]);
console.log("values", editValues);
const [authTokens, setAuthTokens] = useState(
localStorage.getItem("token") || ""
);
const setTokens = (data) => {
localStorage.setItem("token", JSON.stringify(data));
setAuthTokens(data);
// setToken(data['dataValues']['token']);
};
useEffect(() => {
const fetchData = async () => {
try {
//here get the data from api and setstate
setEditValues({ title: "title", shortDesc: "shortDesc" });
} catch (err) {}
};
fetchData();
}, []);
return (
<div>
<form value={{ authTokens, setAuthTokens: setTokens }}>
<input
value={editValues.title || ""}
onChange={(input) => setEditValues({title: input.target.value})}
type="text"
/>
<input
value={editValues.shortDesc}
onChange={(input) => setEditValues({shortDesc: input.target.value})}
type="text"
/>
<button type="submit">send</button>
</form>
</div>
);
};
export default EditArticle;
I must post {input} data to http://localhost:4000/prediction with Axios. But {input} turns undefined.
I am using const instead of class Main extends component. onChange, it sets form data.
const Main = ({ value, suggestions, auth: { user } }) => {
const [formData, setFormData] = useState("");
const [messages, setMessages] = useState([]);
const { input } = formData;
const onChange = e => setFormData(e.target.value);
const onSubmit = event => {
event.preventDefault();
setMessages(prevMsgs => [...prevMsgs, formData]);
console.log({ input });
Axios post.
axios
.post(
`http://localhost:4000/prediction`,
{ input },
{ crossdomain: true }
)
.then(res => {
console.log(res.data);
//setMessages(prevMsgs => [...prevMsgs, formData]);
})
.catch(error => {
console.log(error.message);
});
};
Return (form) with onSubmit, onChange.
return (
<div className="true">
<br />
<form noValidate onSubmit={e => onSubmit(e)}>
<div className="input-group mb-3">
<input
name="input"
type="text"
className="form-control"
placeholder="Type text"
onChange={e => onChange(e)}
/>
)}
<div className="input-group-append">
<button className="btn btn-outline-secondary">Send</button>
</div>
</div>
</form>
</div>
);
};
As I have mentioned in the comment section formData is a string as I see which does not have a property called input what you try to destructure and that's why it is undefined always.
If you really need that format for axios then you can try change the structure of formData with useState as the following first:
const [formData, setFormData] = useState({input: null});
Then maybe you can try updating as:
const onChange = e => setFormData({input: e.target.value});
I hope that helps!
I want to debounce Formik <Field/> but when I type in the field seems debounce does not work. Also I have tried lodash.debounce, throttle-debounce and the same result. How to solve this?
CodeSandbox - https://codesandbox.io/s/priceless-nobel-7p6nt
Snippet:
import ReactDOM from "react-dom";
import { withFormik, Field, Form } from "formik";
const App = ({ setFieldValue }) => {
let timeout;
const [text, setText] = useState("");
const onChange = text => {
if (timeout) clearTimeout(timeout);
timeout = setTimeout(() => setText(text), 750);
};
return (
<Form>
<Field
type="text"
name="textField"
placeholder="Type something..."
onChange={e => {
onChange(e.target.value);
setFieldValue("textField", e.target.value);
}}
style={{ width: "100%" }}
/>
<br />
<br />
<div>output: {text}</div>
</Form>
);
};
const Enhanced = withFormik({
mapPropsToValues: () => ({
textField: ""
}),
handleSubmit: (values, { setSubmitting }) => {
setSubmitting(false);
return false;
}
})(App);
ReactDOM.render(<Enhanced />, document.getElementById("root"));
const [text, setText] = useState("");
const [t, setT] = useState(null);
const onChange = text => {
if (t) clearTimeout(t);
setT(setTimeout(() => setText(text), 750));
};
I would like to suggest to move the call inside of timeout function.
const App = ({ setFieldValue }) => {
let timeout;
const [text, setText] = useState("");
const onChange = text => {
if (timeout) clearTimeout(timeout);
timeout = setTimeout(() => {
setText(text);
//changing value in container
setFieldValue("textField", text);
}, 750);
};
return (
<Form>
<Field
type="text"
name="textField"
placeholder="Type something..."
onChange={e => {
onChange(e.target.value);
}}
style={{ width: "100%" }}
/>
<br />
<br />
<div>output: {text}</div>
</Form>
);
};
Using Custom Hooks
This is abstracted from the answer provided by #Skyrocker
If you find yourself using this pattern a lot you can abstract it out to a custom hook.
hooks/useDebouncedInput.js
const useDebouncedInput = ({ defaultText = '', debounceTime = 750 }) => {
const [text, setText] = useState(defaultText)
const [t, setT] = useState(null)
const onChange = (text) => {
if (t) clearTimeout(t)
setT(setTimeout(() => setText(text), debounceTime))
}
return [text, onChange]
}
export default useDebouncedInput
components/my-component.js
const MyComponent = () => {
const [text, setTextDebounced] = useDebouncedInput({ debounceTime: 200 })
return (
<Form>
<Field
type="text"
name="textField"
placeholder="Type something..."
onChange={(e) => setTextDebounced(e.target.value)}
/>
<div>output: {text}</div>
</Form>
)
}
An Example Using Redux, Fetching, and Validation
Here's a partial example of using a custom hook for a debounced field validator.
Note: I did notice that Field validation seems to not validate onChange but you can expect it onBlur when you leave the field after your debounced update has executed (I did not try racing it or with a long debounce to see what happens). This is likely a bug that should be opened (I'm in the process of opening a ticket).
hooks/use-debounced-validate-access-code.js
const useDebouncedValidateAccessCode = () => {
const [accessCodeLookUpValidation, setAccessCodeLookUpValidation] = useState()
const [debounceAccessCodeLookup, setDebounceAccessCodeLookup] = useState()
const dispatch = useDispatch()
const debouncedValidateAccessCode = (accessCodeKey, debounceTime = 500) => {
if (debounceAccessCodeLookup) clearTimeout(debounceAccessCodeLookup)
setDebounceAccessCodeLookup(
setTimeout(
() =>
setAccessCodeLookUpValidation(
dispatch(getAccessCode(accessCodeKey)) // fetch
.then(() => undefined) // async validation requires undefined for no errors
.catch(() => 'Invalid Access Code'), // async validation expects a string for an error
),
debounceTime,
),
)
return accessCodeLookUpValidation || Promise.resolve(undefined)
}
return debouncedValidateAccessCode
}
some-component.js
const SomeComponent = () => {
const debouncedValidateAccessCode = useDebouncedValidateAccessCode()
return (
<Field
type="text"
name="accessCode"
validate={debouncedValidateAccessCode}
/>
)
}