onSubmit is not executing async function - javascript

I'm trying to submit a function that will generate a gif, when pressing the get gif button.
However, it does not show anything in the console, and the page reloads.
1) I want the client to type in a value
2) set the value to the like so
ex.
http://api.giphy.com/v1/gifs/search?q=USER_VALUE&api_key=iBXhsCDYcnktw8n3WSJvIUQCXRqVv8AP&limit=5
3) fetch the value and return like the following
Current project
https://stackblitz.com/edit/react-4mzteg?file=index.js
App.js
import React, { Component } from 'react';
import logo from './logo.svg';
import './App.css';
import Card from './Card';
import { throws } from 'assert';
class App extends Component {
constructor(props){
super(props);
this.state = {
query: '',
slug:undefined,
url:undefined
}
this.onChange = this.onChange.bind(this);
}
onChange(e){
this.setState({
query: e.target.query
})
}
getGIY = async (e) =>{
try {
const {slug, url} = this.state;
const query = this.state._query
const response = await fetch(`http://api.giphy.com/v1/gifs/search?q=${query}&api_key=iBXhsCDYcnktw8n3WSJvIUQCXRqVv8AP&limit=5`);
const data = await response.json();
const mainData = data.data;
if(query){
this.setState({
slug: mainData[0].title,
url: mainData[0].images.downsized.url
});
console.log(mainData);
}
} catch (error) {
console.log(error);
}
}
render() {
return(
<div>
<h1> Welcome</h1>
<form onSubmit={this.props.getGIY}>
<input type="text" name="query" onChange={this.onChange} ref={(input) => {this.state._query = input}} placeholder="Search GIF..."/>
<button>Get GIF</button>
</form>
<Card slug={this.state.slug} url={this.state.url}/>
</div>
);
}
}
export default App;
Card.js
import React, {Component} from 'react';
const Styles = {
width: '300px',
height: '300px'
}
class Card extends React.Component {
render() {
return (
<div>
<h1>{this.props.slug}</h1>
<div>
<img src={this.props.url}/>
</div>
</div>
);
}
}
export default Card;

You are missing 2 3 4 things
1) instead of this.props.getGIY you need to use this.getGIY
2) as you are using form you need to preventdefault using
getGIY = async (e) =>{
e.preventDefault();
3) instead of e.target.query you need to get e.target.value
4) instead of const query = this.state._query you need to use const query = this.state.query your state name is query
onChange(e){
this.setState({
query: e.target.value
})
}
Demo
Your getGIY function
getGIY = async (e) =>{
e.preventDefault();
try {
const {slug, url} = this.state;
const query = this.state._query
const response = await fetch(`http://api.giphy.com/v1/gifs/search?q=${query}&api_key=iBXhsCDYcnktw8n3WSJvIUQCXRqVv8AP&limit=5`);
const data = await response.json();
const mainData = data.data;
if(query){
this.setState({
slug: mainData[0].title,
url: mainData[0].images.downsized.url
});
console.log(mainData);
}
} catch (error) {
console.log(error);
}
}
Your form
<form onSubmit={this.getGIY}>
<input type="text" name="query" onChange={this.onChange} ref={(input) => {this.state._query = input}} placeholder="Search GIF..."/>
<button>Get GIF</button>
</form>

Mixing promises and try/catch blocks is a little messy as promises themselves duplicate much of the behavior of try/catch blocks. Promises are also chainable. I suggest this edit for your getGIY function. It's just as readable as the existing try/catch w/ unchained promises but more idiomatic (e.g. if THIS is successful, THEN do this next), and more importantly it's a bit more succinct.
getGIY = async (e) =>{
e.preventDefault();
const { query } = this.state;
/* fetch and response.json return promises */
await fetch(`http://api.giphy.com/v1/gifs/search?q=${query}&api_key=iBXhsCDYcnktw8n3WSJvIUQCXRqVv8AP&limit=5`)
// fetch resolved with valid response
.then(response => response.json())
// response.json() resolved with valid JSON data
// ({ data }) is object destructuring (i.e. data.data)
.then(({ data }) => {
this.setState({
slug: data[0].title,
url: data[0].images.downsized.url
});
})
/* use catch block to catch any errors or rejected promises */
.catch(console.log); // any errors sent to log
}

Related

How to update a react component after a fetch

I am learning react.
I have a simple react app sample that :
Fetch users
Once users are fetched, show their name on a Card
What I'd like to do is to expand this sample. Instead of using a simple list of users, I'd like to use a list of pokemons. What I try to do is :
Fetch the list of pokemon and add in state.pokemons
Show the Card with the pokemon name from state.pokemons
From that list, get the URL to fetch the detail of the given pokemon and add in state.pokemonsDetails
From the state.pokemonsDetails, update the Cards list to show the image of the pokemon.
My problem is: I don't even know how to re-render the Cards list after a second fetch.
My question is: How to update the Cards list after the second fetch?
See my code below:
import React from "react";
import CardList from "../components/CardList";
import SearchBox from "../components/SearchBox"
import Scroll from "../components/Scroll"
import './App.css';
class App extends React.Component{
constructor(){
super();
this.state = {
pokemons:[],
pokemonsDetails:[],
searchfield: ''
}
}
getPokemons = async function(){
const response = await fetch('https://pokeapi.co/api/v2/pokemon/?offset=0&limit=20');
const data = await response.json();
this.setState({pokemons:data.results})
}
getPokemonDetails = async function(url){
//fetch function returns a Promise
const response = await fetch(url);
const data = await response.json();
//console.log('getPokemonDetails', data);
this.setState({pokemonsDetails:data});
}
componentDidMount(){
this.getPokemons();
}
onSearchChange = (event) => {
this.setState({searchfield: event.target.value})
}
render(){
const {pokemons, pokemonsDetails, searchfield} = this.state;
if(pokemons.length === 0){
console.log('Loading...');
return <h1>Loading....</h1>
}else if (pokemonsDetails.length === 0){
console.log('Loading details...');
pokemons.map(pokemon => {
return this.getPokemonDetails(pokemon.url);
});
return <h1>Loading details....</h1>
}else{
return(
<div>
<h1>Pokedex</h1>
<SearchBox searchChange={this.onSearchChange}/>
<Scroll>
<CardList pokemons={pokemons}/>
</Scroll>
</div>
);
}
}
}
export default App;
Some remarks :
I can see a problem where my Cards list is first created with state.pokemons, then, I would need to update Cards list with state.pokemonsDetails. The array is not the same.
Second problem, I don't even know how to call the render function after state.pokemonsDetails is filled with the fetch. I set the state, but it looks like render is not called every time
More a question than a remark. The way I update my state in getPokemonDetails might be incorrect. I keep only one detail for one given pokemon. How to keep a list of details? Should I use something else than setState to expand pokemonsDetails array?
You can combine 2 API calls before pokemons state update that would help you to control UI re-renderings better
You can try the below approach with some comments
Side note that I removed pokemonDetails state, so you won't see the loading elements for pokemonDetails as well
import React from "react";
import CardList from "../components/CardList";
import SearchBox from "../components/SearchBox";
import Scroll from "../components/Scroll";
import "./App.css";
class App extends React.Component {
constructor() {
super();
this.state = {
pokemons: [],
searchfield: ""
};
}
getPokemons = async function () {
const response = await fetch(
"https://pokeapi.co/api/v2/pokemon/?offset=0&limit=20"
);
const data = await response.json();
//try to get all pokemon details at once with fetched URLs
const pokemonDetails = await Promise.all(
data.results.map((result) => this.getPokemonDetails(result.url))
);
//map the first and second API response data by names
const mappedPokemons = pokemonDetails.map((pokemon) => {
const pokemonDetail = pokemonDetails.find(
(details) => details.name === pokemon.name
);
return { ...pokemon, ...pokemonDetail };
});
//use mapped pokemons for UI display
this.setState({ pokemons: mappedPokemons });
};
getPokemonDetails = async function (url) {
return fetch(url).then((response) => response.json());
};
componentDidMount() {
this.getPokemons();
}
onSearchChange = (event) => {
this.setState({ searchfield: event.target.value });
};
render() {
const { pokemons, searchfield } = this.state;
if (pokemons.length === 0) {
return <h1>Loading....</h1>;
} else {
return (
<div>
<h1>Pokedex</h1>
<SearchBox searchChange={this.onSearchChange} />
<Scroll>
<CardList pokemons={pokemons} />
</Scroll>
</div>
);
}
}
}
export default App;
Sandbox
If you want to update pokemon details gradually, you can try the below approach
import React from "react";
import CardList from "../components/CardList";
import SearchBox from "../components/SearchBox";
import Scroll from "../components/Scroll";
import "./App.css";
class App extends React.Component {
constructor() {
super();
this.state = {
pokemons: [],
searchfield: ""
};
}
getPokemons = async function () {
const response = await fetch(
"https://pokeapi.co/api/v2/pokemon/?offset=0&limit=20"
);
const data = await response.json();
this.setState({ pokemons: data.results });
for (const { url } of data.results) {
this.getPokemonDetails(url).then((pokemonDetails) => {
this.setState((prevState) => ({
pokemons: prevState.pokemons.map((pokemon) =>
pokemon.name === pokemonDetails.name
? { ...pokemon, ...pokemonDetails }
: pokemon
)
}));
});
}
};
getPokemonDetails = async function (url) {
return fetch(url).then((response) => response.json());
};
componentDidMount() {
this.getPokemons();
}
onSearchChange = (event) => {
this.setState({ searchfield: event.target.value });
};
render() {
const { pokemons, searchfield } = this.state;
if (pokemons.length === 0) {
return <h1>Loading....</h1>;
} else {
return (
<div>
<h1>Pokedex</h1>
<SearchBox searchChange={this.onSearchChange} />
<Scroll>
<CardList pokemons={pokemons} />
</Scroll>
</div>
);
}
}
}
export default App;
Sandbox
Side note that this approach may cause the performance issue because it will keep hitting API for fetching pokemon details multiple times and updating on the same state for UI re-rendering

Newly added items to list do not get _id key prop added until hard refresh

I'm trying to make a todo list using Nextjs and mongodb.
I am able to add items to my list, and they are rendered immediately but the unique id sent with the todo from mongodb isn't applied until the page is refreshed, and I get the warning:
next-dev.js?3515:25 Warning: Each child in a list should have a unique "key" prop.
Check the render method of `HomePage`. See https://reactjs.org/link/warning-keys for more information.
at Todo (webpack-internal:///./components/todo.js:10:17)
at HomePage (webpack-internal:///./pages/index.js:28:21)
at MyApp (webpack-internal:///./pages/_app.js:15:24)
at ErrorBoundary (webpack-internal:///./node_modules/next/dist/compiled/#next/react-dev-overlay/client.js:8:20638)
at ReactDevOverlay (webpack-internal:///./node_modules/next/dist/compiled/#next/react-dev-overlay/client.js:8:23179)
at Container (webpack-internal:///./node_modules/next/dist/client/index.js:241:5)
at AppContainer (webpack-internal:///./node_modules/next/dist/client/index.js:830:24)
at Root (webpack-internal:///./node_modules/next/dist/client/index.js:983:26)
I suspect it is an async issue but after trying to figure it out myself and online I am hoping someone will shine some light on why this is happening.
Here is my backend code:
import {
getAllTodos,
insertTodo,
connectDatabase,
} from "../../helpers/db-util";
async function handler(req, res) {
let client;
try {
client = await connectDatabase();
} catch (error) {
res
.status(500)
.json({ message: error.message || "Error connecting to MongoDB." });
return;
}
if (req.method === "GET") {
try {
const todosList = await getAllTodos("todos");
res.status(200).json({ todos: todosList });
} catch (error) {
res.status(500).json({
message: error.message || "Unable to fetch todos from database.",
});
}
}
if (req.method === "POST") {
const { text } = req.body;
if (!text || text.trim() === "") {
res
.status(422)
.json({ message: "You must not have a todo with no text." });
client.close();
return;
}
const newTodo = { text };
let result;
let result2;
try {
result = await insertTodo(client, "todos", newTodo);
result2 = await getAllTodos("todos");
res.status(201).json({
message: "Todo successfully added!",
todo: newTodo,
todos: result2,
});
// console.log(result, result2);
} catch (error) {
res.status(500).json({ message: error.message || "Unable to add todo." });
}
}
client.close();
}
export default handler;
and here is the code to the helper functions they utilize:
import { MongoClient } from "mongodb";
const connectionString = `mongodb+srv://${process.env.mongodb_username}:${process.env.DB_PASS}#${process.env.mongodb_clustername}.e79y2.mongodb.net/${process.env.mongodb_db_name}?retryWrites=true&w=majority`;
export async function connectDatabase() {
const client = await MongoClient.connect(connectionString);
return client;
}
export async function insertTodo(client, collection, todo) {
const db = client.db();
const result = await db.collection(collection).insertOne(todo);
return result;
}
export async function getAllTodos(collection) {
const client = await connectDatabase();
const db = client.db();
const todos = await db.collection(collection).find().toArray();
return todos;
}
On the front end I initially load the todos with getServerSideProps and all of those have the key of _id properly applied, but when I add a new todo I get the key warning, and even though in a console.log of the newly created item will show the _id as apart of the todo object, it isn't applied to the list of todos that is mapped over.
export default function HomePage(props) {
const { todos } = props;
const [todosList, setTodosList] = useState([]);
const [isLoading, setIsLoading] = useState(false);
function getTodos() {
const parsedTodos = JSON.parse(todos);
setTodosList(parsedTodos);
}
useEffect(() => {
getTodos();
}, [todos]);
return (
<div>
<Head>
<title>Next Todos</title>
<meta name="description" content="NextJS todos app" />
</Head>
<main>
<TodoInput
getTodos={getTodos}
setTodosList={setTodosList}
todosList={todosList}
setIsLoading={setIsLoading}
/>
{!isLoading &&
todosList.map((todo) => (
<Todo key={todo._id} id={todo._id} text={todo.text} />
))}
</main>
</div>
);
}
export async function getServerSideProps(context) {
let client;
client = await connectDatabase();
// console.log(client);
const todosList = await getAllTodos("todos");
const allTodos = JSON.stringify(todosList);
return {
props: {
todos: allTodos,
},
};
}
and here is the Todos input form and submit handler:
const TodoInput = (props) => {
async function postNewTodo(enteredTodo) {
await fetch("/api/todos", {
method: "POST",
body: JSON.stringify({ text: enteredTodo }),
headers: { "Content-Type": "application/json" },
})
.then((response) => response.json())
.then((data) => {
props.setIsLoading(true);
console.log(data.todos);
props.setTodosList([
...props.todosList,
{ id: data.todo._id, text: data.todo.text },
]);
props.setIsLoading(false);
});
}
const todoInputRef = useRef();
const handleSubmit = (e) => {
e.preventDefault();
const enteredTodo = todoInputRef.current.value;
postNewTodo(enteredTodo);
// props.setTodosList([...props.todosList, { text: enteredTodo }]);
};
return (
<Fragment>
<form onSubmit={handleSubmit}>
<input type="text" required ref={todoInputRef} />
<button>Add Todo</button>
</form>
</Fragment>
);
};
export default TodoInput;
I tried to use a loading state to slow down the mapping of the newly added object so that it would have time to properly have its key applied to no avail.
Any help as to why this is happening would be greatly appreciated.
as can be seen in your code,at the time when you add todo you are haveing id as member of todo item but at the time when you render items you are using
_id,which is undefined and so all the todo items in array have same value for _id = undefined,so using id instead of _id will clear the warning like this
<main>
<TodoInput
getTodos={getTodos}
setTodosList={setTodosList}
todosList={todosList}
setIsLoading={setIsLoading}
/>
{!isLoading &&
todosList.map((todo) => (
<Todo key={todo.id} id={todo.id} text={todo.text} />
))}
</main>
I do not much about nextjs! Here is a react option!
Since the data your using is coming in an async way try calling useEffect with async too
async function getTodos() {
const parsedTodos = await JSON.parse(todos);
setTodosList(parsedTodos);
}
or if your not going to the key use this
<main>
<TodoInput
getTodos={getTodos}
setTodosList={setTodosList}
todosList={todosList}
setIsLoading={setIsLoading}
/>
{!isLoading &&
todosList.map((todo, i) => (
<Todo key={i} id={todo._id} text={todo.text} />
))}
</main>
Where i is the todo index in that array
Solved!
props.setTodosList([
...props.todosList,
{ _id: data.todo._id, text: data.todo.text },
]);
in todo-input.js was initially labeled as id instead of _id.
Thank you to the two of you who tried to help me with this.

Making an axios get request and using React useState but when logging the data it still shows null

When I make a request to an API and setting the state to the results from the Axios request it still shows up null. I am using React useState and setting the results from the request and wanting to check to see if its coming through correctly and getting the right data its still resulting into null. The request is correct but when I use .then() to set the state that is the issue I am having.
Below is the component that I am building to make the request called Details.js (first code block) and the child component is the DetailInfo.js file (second code block) that will be displaying the data. What am I missing exactly or could do better when making the request and setting the state correctly display the data?
import React, {useEffect, useState} from 'react';
import { Col, Container, Row } from 'react-bootstrap';
import axios from 'axios';
import { getCookie } from '../utils/util';
import DetailInfo from '../components/DetailInfo';
import DetailImage from '../components/DetailImage';
const Details = () => {
const [ countryData, setCountryData ] = useState(null);
let country;
let queryURL = `https://restcountries.eu/rest/v2/name/`;
useEffect(() => {
country = getCookie('title');
console.log(country);
queryURL += country;
console.log(queryURL);
axios.get(queryURL)
.then((res) => {
console.log(res.data[0])
setCountryData(res.data[0]);
})
.then(() => {
console.log(countryData)
}
);
}, [])
return (
<>
<Container className="details">
<Row>
<Col sm={6}>
<DetailImage />
</Col>
<Col sm={6}>
<DetailInfo
name={countryData.name}
population={countryData.population}
region={countryData.region}
subRegion={countryData.subRegion}
capital={countryData.capital}
topLevelDomain={countryData.topLevelDomain}
currencies={countryData.currencies}
language={countryData.language}
/>
</Col>
</Row>
</Container>
</>
)
}
export default Details;
The child component below......
import React from 'react';
const DetailInfo = (props) => {
const {name, population, region, subRegion, capital, topLevelDomain, currencies, language} = props;
return (
<>detail info{name}{population} {region} {capital} {subRegion} {topLevelDomain} {currencies} {language}</>
)
}
export default DetailInfo;
Ultimately, the problem comes down to not handling the intermediate states of your component.
For components that show remote data, you start out in a "loading" or "pending" state. In this state, you show a message to the user saying that it's loading, show a Spinner (or other throbber), or simply hide the component. Once the data is retrieved, you then update your state with the new data. If it failed, you then update your state with information about the error.
const [ dataInfo, setDataInfo ] = useState(/* default dataInfo: */ {
status: "loading",
data: null,
error: null
});
useEffect(() => {
let unsubscribed = false;
fetchData()
.then((response) => {
if (unsubscribed) return; // unsubscribed? do nothing.
setDataInfo({
status: "fetched",
data: response.data,
error: null
});
})
.catch((err) => {
if (unsubscribed) return; // unsubscribed? do nothing.
console.error('Failed to fetch remote data: ', err);
setDataInfo({
status: "error",
data: null,
error: err
});
});
return () => unsubscribed = true;
}, []);
switch (dataInfo.status) {
case "loading":
return null; // hides component
case "error":
return (
<div class="error">
Failed to retrieve data: {dataInfo.error.message}
</div>
);
}
// render data using dataInfo.data
return (
/* ... */
);
If this looks like a lot of boiler plate, there are useAsyncEffect implementations like #react-hook/async and use-async-effect that handle it for you, reducing the above code to just:
import {useAsyncEffect} from '#react-hook/async'
/* ... */
const {status, error, value} = useAsyncEffect(() => {
return fetchData()
.then((response) => response.data);
}, []);
switch (status) {
case "loading":
return null; // hides component
case "error":
return (
<div class="error">
Failed to retrieve data: {error.message}
</div>
);
}
// render data using value
return (
/* ... */
);
Because state only update when component re-render. So you should put console.log into useEffect to check the new value:
useEffect(() => {
country = getCookie('title');
console.log(country);
queryURL += country;
console.log(queryURL);
axios.get(queryURL).then(res => {
console.log(res.data[0]);
setCountryData(res.data[0]);
});
}, []);
useEffect(() => {
console.log(countryData);
}, [countryData]);
useState does reflecting its change immediately.
I think that it would be probably solved if you set countryData to second argument of useEffect.
useEffect(() => {
country = getCookie('title');
console.log(country);
queryURL += country;
console.log(queryURL);
axios.get(queryURL)
.then((res) => {
console.log(res.data[0])
setCountryData(res.data[0]);
})
.then(() => {
console.log(countryData)
}
);
}, [countryData])
The issue is, as samthecodingman, pointed out, an issue of intermediate data. Your component is being rendered before the data is available, so your child component needs to re-render when its props change. This can be done via optional chaining, an ES6 feature.
import React, { useEffect, useState } from "react";
import DetailInfo from "./DetailInfo";
import { Col, Container, Row } from "react-bootstrap";
import axios from "axios";
const Details = () => {
const [countryData, setCountryData] = useState({});
let country = "USA";
let queryURL = `https://restcountries.eu/rest/v2/name/`;
useEffect(() => {
console.log(country);
queryURL += country;
console.log(queryURL);
axios
.get(queryURL)
.then((res) => {
console.log(res.data[0]);
setCountryData(res.data[0]);
})
.then(() => {
console.log(countryData);
});
}, []);
return (
<Container className="details">
<Row>
<Col sm={6}>
<DetailInfo
name={countryData?.name}
population={countryData?.population}
region={countryData?.region}
subRegion={countryData?.subRegion}
capital={countryData?.capital}
language={countryData?.language}
/>
</Col>
<Col sm={6}></Col>
</Row>
</Container>
);
};
export default Details;
Checkout my Codesandbox here for an example.

Mounting Data into React State Returning Error

I have some data which I'm calling from an API. I was to update the data into my declared state which some of it works. But one of my state data is returning an error that "Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in the component will unmount method."
Below is my Image Data file. The line giving the error is commented out in the return method.
import React, { Component } from "react";
import axios from "axios";
import { Card } from 'react-bootstrap';
export default class imagedata extends Component {
state = {
img: "",
infodata: [],
alldata: [],
};
componentDidMount() {
this.fetchCatDetails();
}
fetchCatDetails = async () => {
const info = `https://api.thecatapi.com/v1/breeds`;
const url = `https://api.thecatapi.com/v1/images/search?breed_id=${this.props.option.id}`;
try {
const response = await axios.get(url);
// console.log(response)
const img = await response.data[0].url;
// console.log(alldata)
this.setState({
img
});
const allresult = await axios.get(url);
const alldata = await allresult.data[0];
this.setState({
alldata,
});
const response1 = await axios.get(info);
const infodata = await response1.data;
// console.log(infodata)
this.setState({
infodata,
});
} catch (error) {
console.log(error);
}
};
render() {
const {
option: {
name,
origin,
temperament,
life_span,
weight: { metric },
description,
},
} = this.props;
return (
<div className="center">
<img src={this.state.img} alt="" loading="" className="img"/> <br/>
{name} <br/>
{origin} <br/>
{description}
{this.state.alldata.map((item, id) => (
<div key={id}>
{/* This line below is the line returning the error */}
{console.log(item)}
</div>
))}
</div>
);
}
}
Here is my index.js
import React, { Component } from "react";
import ReactDOM from "react-dom";
import axios from "axios";
import Imagedata from "./Imagedata";
import './style.css'
class App extends Component {
state = {
data: [],
};
componentDidMount() {
this.fetchData();
}
fetchData = async () => {
const url = "https://api.thecatapi.com/v1/breeds";
try {
const response = await axios.get(url);
const data = await response.data;
this.setState({
data,
});
} catch (error) {
console.log(error);
}
};
render() {
return (
<div className="App">
<h1>React Component Life Cycle</h1>
<h1>Calling API</h1>
<div>
{this.state.data.map((item, id) => (
<div key={id}>
<Imagedata option={item} />
</div>
))}
</div>
</div>
);
}
}
ReactDOM.render(<App />, document.getElementById("root"));

How can I search an array with React apollo-client cache?

I have a simple search component and handleSearch function:
const { data, loading, error } = useQuery(QUERY_GET_ELEMENTS);
const client = useApolloClient();
<input
onChange={handleSearch}
placeholder="🔎 Search..."
/>
function handleSearch(e) {
const { value } = e.target;
const matchingElements = data.filter(({ name }) =>
name.toLowerCase().includes(value.toLowerCase())
);
client.writeData({
data: {
elements: matchingElements
}
});
}
// rendering the elements looks something like this:
data.elements.map(el => <div>{el.name}</div>
The data comes from a useQuery hook.
The problem is that the search only works in one direction as once the elements are filtered I lose the original list. I need to keep a store of all of the elements that I can filter and render only the filtered ones while persisting the original list.
I'm using apollo for state management and cannot seem to get this working. My first thought was to use client.writeData to duplicate the elements and that would never be modified, however this did not work as expected.
Any help is much appreciated.
You should be able to accomplish this with the useState hook. This example works for me:
import React, { useState, useEffect } from 'react';
import gql from 'graphql-tag';
import { useQuery } from '#apollo/react-hooks'
const QUERY_GET_ELEMENTS = gql`
{
elements {
id
name
}
}
`;
export default function Test() {
const [isDisplayDataSet, setIsDisplayDataSet] = useState(false);
const [displayData, setDisplayData] = useState([]);
const { data, loading, error } = useQuery(QUERY_GET_ELEMENTS);
useEffect(() => {
if (!loading && !isDisplayDataSet) {
setDisplayData(data.elements);
setIsDisplayDataSet(true);
}
}, [isDisplayDataSet, displayData, data, loading])
function handleSearch(e) {
const { value } = e.target;
const matchingElements = data.elements.filter(({ name }) =>
name.toLowerCase().includes(value.toLowerCase())
);
setDisplayData(matchingElements);
}
if (error) {
console.error(error);
return <h1>There was an error</h1>
}
if (isDisplayDataSet) {
return (
<>
<input
className="form-control mb-3"
onChange={handleSearch}
placeholder="🔎 Search..."
/>
<ul className="list-group">
{displayData.map(el => <li className="list-group-item" key={el.id}>{el.name}</li>)}
</ul>
</>
);
} else {
return '';
}
}
I added some bootstrap classes for styling :)
And here is the quick-and-dirty apollo-server I setup to load some data in:
const { ApolloServer } = require('apollo-server');
const gql = require('graphql-tag');
const fetch = require('node-fetch');
const typeDefs = gql`
type Element {
id: ID!
name: String!
}
type Query {
elements: [Element]!
}
schema {
query: Query
}
`;
const resolvers = {
Query: {
async elements() {
const res = await fetch('https://reqres.in/api/users');
const { data } = await res.json();
const elements = data.map(({ id, first_name, last_name }) => ({ id, name: `${first_name} ${last_name}` }))
console.log('elements', elements);
return elements;
}
}
}
const server = new ApolloServer({
typeDefs,
resolvers
});
server.listen().then(({ url }) => {
console.log('server ready on ' + url);
});

Categories