I'm trying t0 create a simple demo using React, Graphql, Typescipt and useMutation hook.
I have a mongoDB set up and a useQury hook set up that all works and I can output the return from useQuery.
I'm also using apollo:gernerate to generate types
The problem I have is getting the useMutation to work.
App.tsx
import React, { useState } from 'react';
import './App.css';
import { RecipeData } from '../generated/RecipeData';
import { GET_ALL_RECIPES, ADD_RECIPE } from '../queries';
import { useQuery, useMutation } from 'react-apollo-hooks';
const App: React.FC = () => {
const [name, setName] = useState<string>('')
const [description, setDes] = useState<string>('')
const handleNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setName(e.target.value)
}
const handleDesChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setDes(e.target.value)
}
const handleClick = (e: React.ButtonHTMLAttributes<HTMLButtonElement>) => {
const [createRecipe] = useMutation(
ADD_RECIPE,{
onCompleted(data) {
confirm(data);
}
}
);
}
const { data, loading } = useQuery<RecipeData | null>(GET_ALL_RECIPES, {
suspend: false
})
if (loading || !data) return <div>Loading</div>
return (
<div className="App">
<h1>Graphql</h1>
<ul>
{
data.recipe !== null && data.recipe.map((recipe, i) => (
<li key={i}>{recipe.name}</li>
))
}
</ul>
<form>
<div>
<label>Name</label>
<input
type="text"
value={name}
onChange={handleNameChange}
/>
</div>
<div>
<label>Description</label>
<input
type="text"
value={description}
onChange={handleDesChange}
/>
</div>
<button onClick={handleClick}>Add Recipe</button>
</form>
</div>
);
}
export default App;
queries/index.tsx
import { gql } from 'apollo-boost';
export const GET_ALL_RECIPES = gql`
query RecipeData{
recipe{
_id
name
description
}
}
`
export const ADD_RECIPE = gql`
mutation AddRecipe($type: String){
addRecipe(type: $type){
name
description
}
}
`
generated/AddRecipe.ts
export interface AddRecipe_addRecipe {
__typename: "Recipe";
name: string | null;
description: string | null;
}
export interface AddRecipe {
addRecipe: AddRecipe_addRecipe | null;
}
export interface AddRecipeVariables {
type?: string | null;
}
You are using useMutation hook in a wrong way.
Your current code:
const handleClick = (e: React.ButtonHTMLAttributes<HTMLButtonElement>) => {
const [createRecipe] = useMutation(ADD_RECIPE, {
onCompleted(data) {
confirm(data);
}
});
};
Now extract useMutation hook call outside the handleClick function and call the createRecipe that is the actual function to call when you click in the correct way:
const [createRecipe] = useMutation(ADD_RECIPE, {
onCompleted(data) {
confirm(data);
}
});
const handleClick = (e: React.ButtonHTMLAttributes<HTMLButtonElement>) => {
createRecipe({ variables: { name, description }})
};
You have to use react hooks at the top level of your components, don't put them inside functions, loops or conditional statements :)
Documentation here: https://www.apollographql.com/docs/react/data/mutations/
Related
I'm trying to map over data from API, but while writing the code to display the data I got this error: TypeError: weatherData.map is not a function
I tried removing useEffect from the code and tried to add curly brackets: const [weatherData, setWeatherData] = useState([{}])
Update: Line 14 log undefined : console.log(weatherData.response)
import axios from 'axios'
import { useEffect, useState } from 'react'
import './App.css'
function App() {
const [search, setSearch] = useState("london")
const [weatherData, setWeatherData] = useState([])
const getWeatherData = async () => {
try {
const weatherData = await axios.get(`https://api.openweathermap.org/data/2.5/weather?q=${search}&appid={APIKEY}`);
console.log(weatherData.response);
if (weatherData) {
setWeatherData(weatherData);
}
} catch (err) {
console.error(err);
}
}
useEffect(() => {
getWeatherData()
}, [getWeatherData])
const handleChange = (e) => {
setSearch(e.target.value)
}
return (
<div className="App">
<div className='inputContainer'>
<input className='searchInput' type="text" onChange={handleChange} />
</div>
{weatherData.map((weather) => {
return (
<div>
<h1>{weather.name}, {weather.country}</h1>
</div>
)
})}
</div>
)
}
export default App
You're having errors in fetching the data as well as rendering it.
Just change the entire App component like this :
import { useEffect, useState } from "react";
import axios from "axios";
function App() {
const [search, setSearch] = useState("London");
const [weatherData, setWeatherData] = useState([]);
const APIKEY = "pass your api key here";
useEffect(() => {
const fetchData = async () => {
const result = await axios(
`https://api.openweathermap.org/data/2.5/weather?q=${search}&appid=${APIKEY}`
);
setWeatherData(result.data);
};
fetchData();
}, [search]);
const handleChange = (e) => {
setSearch(e.target.value);
};
return (
<div className="App">
<div className="inputContainer">
<input className="searchInput" type="text" onChange={handleChange} />
</div>
<h1>
{" "}
{weatherData.name} ,{" "}
{weatherData.sys ? <span>{weatherData.sys.country}</span> : ""}{" "}
</h1>
</div>
);
}
export default App;
this should be working fine just make sure to change : const APIKEY = "pass your api key "; to const APIKEY = "<your API key> ";
this is a demo in codesandbox
Create a promise function:
const getWeatherData = async () => {
try {
const weatherData = await axios.get(`https://api.openweathermap.org/data/2.5/weather?q=${search}&appid={APIKEY}`);
console.log(weatherData.response);
if (weatherData.response.data) {
setWeatherData(weatherData.response.data);
}
} catch (err) {
console.error(err);
}
}
Then call it.
import React, { useState, useEffect } from 'react';
import Axios from 'axios';
import './App.css';
import { Users } from "../App";
export interface Users {
id: number;
author: string;
download_url: string;
url: string;
page: string;
}
function searchBar() {
const [data, setData] = useState([]);
const [input, setInput] = useState('');
const [output, setOutput] = useState([])
useEffect(() => {
async function getData() {
const res = await Axios.get("https://picsum.photos/v2/list/author")
setData(res.data)
}
getData()
}, [])
useEffect(() => {
setOutput([])
data.filter(val => {
if (val.data.toLowerCase().includes(input.toLowerCase())) {
setOutput(output => [...output, val])
}
})
}, [input])
return (
<div className="input">
{/* search bar */}
<div className="search-bar" key={user.author} >
<input onChange={ e => setInput(e.target.value) }
type = "text" placeholder = 'Search' />
</div>
< div className = "output" >
{
output.map((user) => (
{user.author}
))
}
</div>
</div>
)
}
export default searchBar;
My React component loads infinitely and I want it to load only depending on the data that I get from the database, the console.log("1") is only for testing how many times the component loads.
This is the component:
import React from "react";
import Axios from "axios";
import { useState, useEffect } from "react";
function Added() {
const [data, setData] = useState([]);
useEffect(() => {
Axios.get("http://localhost:3001/").then((result) => {
setData(result.data);
});
}, [data]);
console.log("1");
return data.map((item) => {
return (
<div key={item._id}>
<h1>{item.finame}</h1>
<h1>{item.laname}</h1>
<h5>{item.age}</h5>
</div>
);
});
}
export default Added;
This is where it loads:
import "./App.css";
import { useState, useReducer, useEffect } from "react";
import Added from "./added";
import Axios from "axios";
function App() {
const GettingALlTheData = () => {
return Axios.get("http://localhost:3001/").then((result) => {
return result.data;
});
};
/* -------------------- For the useReducer -------------------- */
const Actions = {
Add: "add",
};
const defaultState = {
list: [GettingALlTheData],
};
console.log(defaultState);
const reducer = (state, action) => {
switch (action.type) {
case Actions.Add:
const listItem = action.payload;
try {
Axios.post("http://localhost:3001/add", listItem);
} catch (error) {
console.log(error + "444444");
}
return { ...state, list: [...state.list, listItem] };
default:
console.log("this is the default");
}
};
const [state, dispatch] = useReducer(reducer, defaultState);
/* ---------------------------- For the form ---------------------------- */
const [listItem, setListItem] = useState({ finame: "", laname: "", age: 0 });
const [list, setList] = useState([]);
useEffect(() => {
Axios.get("http://localhost:3001/").then((result) => {
state.list = result.data;
});
}, [state.list]);
const handelChange = (e) => {
const name = e.target.name;
const value = e.target.value;
setListItem({ ...listItem, [name]: value });
};
const handelSubmit = (e) => {
e.preventDefault();
dispatch({ type: Actions.Add, payload: listItem });
};
const [data, setData] = useState({});
/* -------- for the useEffect to get the Data from the server -------- */
/* ------------------------ for the form return ---------------------- */
return (
<div className="App">
<h1>CRUD app using MERN stack</h1>
<form onSubmit={handelSubmit}>
<label htmlFor="finame">First name:</label>
<input
type="text"
name="finame"
id="finame"
value={listItem.finame}
onChange={handelChange}
/>
<label htmlFor="laname">Last name:</label>
<input
type="text"
name="laname"
id="laname"
value={listItem.laname}
onChange={handelChange}
/>
<label htmlFor="age">Age:</label>
<input
type="Number"
name="age"
id="age"
value={listItem.age}
onChange={handelChange}
/>
<button type="Submit">Submit</button>
</form>
{state.list ? (
<Added />
) : (
state.list.map((listItem) => {
return (
<div key={listItem._id}>
<h1>First name : {listItem.finame}</h1>
<h1>Last name: {listItem.laname}</h1>
<h3>Age: {listItem.age}</h3>
</div>
);
})
)}
</div>
);
}
export default App;
That's because you use the useEffect function with no dependency, so it is executed every time any prop/state changes (it's like a class component's componentDidUpdate).
I suggest you use it inside your Added component like a componentDidMount, so that it only execute once. To do it, you have to pass an empty dependency array, like so:
useEffect(() => {
//fetching the data
}, []);
My React App is crashing when fetching searchResults from API, I have checked the API urls wise search queries and it works perfectly however when i try to send input via React and display results it crashes and even freezes my PC. I dont understand whats going on here. I have fetched results from the API in React without search query and it works. So the API works when used via Curl and React app can fetch and display all the data but unable to display specific data. Below is my code:
function Search() {
const [data, setData] = React.useState([]);
const [searchTerm, setSearchTerm] = React.useState("");
const handleChange = e => {
setSearchTerm(e.target.value);
};
React.useEffect(() => {
if (searchTerm) {
getData(searchTerm);
}
});
const getData = (searchTerm) => {
axios.get("http://localhost:8000/SearchPost/?search="+searchTerm)
.then(res => (setData(res.data)))
}
return (
<div className="App">
<input
type="text"
placeholder="Search"
value={searchTerm}
onChange={handleChange}
/>
<ul>
{data.map(item => (
<li>{item.co_N}</li>
))}
</ul>
</div>
);
}
export default Search;
One solution is to "debounce" setting searchTerm to minimize the request to the API:
we're going to use lodash package particularly it's debounce method (doc here), and useCallback from Hooks API (doc here) :
import React, { useState, useCallback, useRef } from "react";
import _ from "lodash";
import axios from "axios";
import TextField from "#material-ui/core/TextField";
const SearchInputComponent = ({ label }) => {
const [value, setValue] = useState("");
const [data, setData] = useState([]);
const inputRef = useRef(null);
const debounceLoadData = useCallback(
_.debounce((value) => {
getData(value);
}, 500), // you can set a higher value if you want
[]
);
const getData = (name) => {
axios.get(`https://restcountries.eu/rest/v2/name/${name}`).then((res) => {
console.log(res);
setData(res.data);
});
};
const handleSearchFieldChange = (event) => {
const { value } = event.target;
setValue(value);
debounceLoadData(value);
};
return (
<>
<TextField
inputRef={inputRef}
id="searchField"
value={value}
label={"search"}
onChange={handleSearchFieldChange}
/>
{data &&
<ul>
{data.map(country=> (
<li key={country.alpha3Code}>{country.name}</li>
))
}
</ul>
}
</>
);
};
export default SearchInputComponent;
with this code the front end will wait 500 ms before fetching api with the search input value.
here a sandBox example.
Possible Feature: Make search field generic
If in the future you will need a search component you can make it generic with Context:
first create a context file named for example SearchInputContext.js and add:
SearchInputContext.js
import React, {
createContext,
useState
} from 'react';
export const SearchInputContext = createContext({});
export const SearchInputContextProvider = ({ children }) => {
const [value, setValue] = useState('');
return (
<SearchInputContext.Provider
value={{ searchValue: value, setSearchValue: setValue }}
>
{children}
</SearchInputContext.Provider>
);
};
Next create a generic searchField component named for example SearchInput.js and add in it :
SearchInput.js
import React, {
useState,
useCallback,
useRef,
useContext
} from 'react';
import _ from 'lodash';
import TextField from "#material-ui/core/TextField";
import { SearchInputContext } from './SearchInputContext';
const SearchInputComponent = () => {
const [value, setValue] = useState('');
const { setSearchValue } = useContext(SearchInputContext);
const inputRef = useRef(null);
const debounceLoadData = useCallback(
_.debounce((value) => {
setSearchValue(value);
}, 500),
[]
);
const handleSearchFieldChange = (event) => {
const { value } = event.target;
setValue(value);
debounceLoadData(value);
};
return (
<>
<TextField
inputRef={inputRef}
id="searchField"
value={value}
label={"search"}
onChange={handleSearchFieldChange}
/>
</>
);
};
export default SearchInputComponent;
After in your App.js (or other component page where you want a searchField) add your ContextProvider like this:
App.js
import {ListPage} from "./searchPage";
import {SearchInputContextProvider} from './SearchInputContext';
import "./styles.css";
export default function App() {
return (
<SearchInputContextProvider>
<ListPage/>
</SearchInputContextProvider>
);
}
And finally add your searchComponent where you need a search feature like in the ListPage component :
SearchPage.js:
import React, { useState,useContext, useEffect } from "react";
import axios from "axios";
import SearchInputComponent from './SearchInput';
import {SearchInputContext} from './SearchInputContext'
export const ListPage = () => {
const [data, setData] = useState([]);
const { searchValue } = useContext(SearchInputContext);
useEffect(() => {
if(searchValue){
const getData = (name) => {
axios.get(`https://restcountries.eu/rest/v2/name/${name}`).then((res) => {
console.log(res);
setData(res.data);
});
};
return getData(searchValue)
}
}, [ searchValue]);
return (
<>
<SearchInputComponent />
{data &&
<ul>
{data.map(country=> (
<li key={country.alpha3Code}>{country.name}</li>
))
}
</ul>
}
</>
);
};
here a sandbox link of this example
I useMutation to send message ,but the message list in chat window not change. I found that the cache has changed . Please help , I can't understand.
The useQuery not work . UI have no change :(
But~! When I put them in one js file. it works.... why???
The version I used is #apollo/react-hooks 3.1.1
Parent window.js
import React from 'react';
import { useQuery } from "#apollo/react-hooks";
import { GET_CHAT } from "#/apollo/graphql";
import ChatInput from "#/pages/chat/components/input";
const ChatWindow = (props) => {
const { chatId, closeChat } = props;
const { data, loading, error } = useQuery(GET_CHAT, { variables: { chatId: chatId } });
if (loading) return <p>Loading...</p>;
if (error) return <p>{error.message}</p>;
const { chat } = data;
return (
<div className="chatWindow" key={'chatWindow' + chatId}>
<div className="header">
<span>{chat.users[1].username}</span>
<button className="close" onClick={() => closeChat(chatId)}>X</button>
</div>
<div className="messages">
{chat.messages.map((message, j) =>
<div key={'message' + message.id} className={'message ' + (message.user.id > 1 ? 'left' : 'right')}>
{message.text}
</div>
)}
</div>
<div className="input">
<ChatInput chatId={chatId}/>
</div>
</div>
);
};
export default ChatWindow;
Child input.js
import React, { useState } from 'react';
import { useApolloClient, useMutation } from "#apollo/react-hooks";
import { ADD_MESSAGE, GET_CHAT } from "#/apollo/graphql";
const ChatInput = (props) => {
const [textInput, setTextInput] = useState('');
const client = useApolloClient();
const { chatId } = props;
const [addMessage] = useMutation(ADD_MESSAGE, {
update(cache, { data: { addMessage } }) {
const { chat } = client.readQuery({
query: GET_CHAT,
variables: {
chatId: chatId
}
});
chat.messages.push(addMessage);
client.writeQuery({
query: GET_CHAT,
variables: {
chatId: chatId
},
data: {
chat
}
});
}
});
const onChangeInput = (event) => {
event.preventDefault();
setTextInput(event.target.value);
};
const handleKeyPress = (event, chatId, addMessage) => {
if (event.key === 'Enter' && textInput.length) {
addMessage({
variables: {
message: {
text: textInput,
chatId: chatId
}
}
});
setTextInput('');
}
};
return (
<input type="text"
value={textInput}
onChange={(event) => onChangeInput(event)}
onKeyPress={(event) => handleKeyPress(event, chatId, addMessage)}
/>
);
};
export default ChatInput;
You probably solved the issue by now, but for the record:
Your code mutates the chat state:
chat.messages.push(addMessage);
State should not be mutated (see the React setState Docs for more details).
Contruct a new array instead:
const newChat = [...chat, addMessage]