I'm trying to get a handle on the new tRPC version 10 with a basic shopping list CRUD Nextjs app. I have successfully set up the tRPC endpoint with "get all" and a "create" handlers and can confirm that they both work after testing from the front end. However, I can't seem to update my state with the data from the "get all" call. In older tRPC versions we would have updated the state as follows:
const data = trpc.useQuery(["items.getAll"], {
onSuccess(items) {
setItems(items);
},
});
In version 10 however, they've done away with the useQuery() arguments in favour of conditional status returns according to docs. I tried updating the state as follows:
const [items, setItems] = useState<ShoppingItem[]>([]);
const data = trpc.shoppingItem.getAll.useQuery();
if (data.isSuccess) {
setItems(data.data);
}
This understandably causes a "Too many re-renders" error since each time the state updates it re-renders the component, therefore triggering a new isSuccess and re-updating the state.
What is the proper way to update state from tRPCv10?
My full component follows for context:
import { useState, useEffect } from "react";
import { ShoppingItem } from "#prisma/client";
import type { NextPage } from "next";
import Head from "next/head";
import ItemModal from "../components/ItemModal";
import { trpc } from "../utils/trpc";
const Home: NextPage = () => {
const [items, setItems] = useState<ShoppingItem[]>([]);
const [modalOpen, setModalOpen] = useState<boolean>(false);
const data = trpc.shoppingItem.getAll.useQuery();
if (data.isSuccess) {
setItems(data.data);
}
return (
<>
<Head>
<title>Shopping List</title>
<meta name="description" content="Generated by create-t3-app" />
<link rel="icon" href="/favicon.ico" />
</Head>
{modalOpen && (
<ItemModal setModalOpen={setModalOpen} setItems={"hello"} />
)}
<main className="mx-auto my-12 max-w-3xl">
<div className="flex justify-between">
<h2 className="text-2xl font-semibold">My Shopping List</h2>
<button
className="rounded-md bg-violet-500 p-2 text-sm text-white transition hover:bg-violet-600"
type="button"
onClick={() => setModalOpen(true)}
>
Add Item
</button>
</div>
<ul className="mt-4">
{items.map((item) => (
<li key={item.id}>{item.name}</li>
))}
</ul>
</main>
</>
);
};
export default Home;
I had the same problem and managed to get it to work by adding 'undefined' as an argument to useQuery().
const { data: itemsData, isLoading } = trpc.items.getAllItems.useQuery(undefined, {
onSuccess(items) {
setItems(items)
}
})
Not sure if this is the correct way to update state in tRPC v10 but it seems to be working. Let me know if you find the correct way to do it!
I'm not sure if this is the intended way to deal with this but I managed to get it working as follows:
const [items, setItems] = useState<ShoppingItem[]>([]);
const [modalOpen, setModalOpen] = useState<boolean>(false);
const { data: shoppingItems } = trpc.shoppingItem.getAll.useQuery();
useEffect(() => {
shoppingItems && setItems(shoppingItems);
}, [shoppingItems]);
I call useEffect to update the state when there is a change in the data pulled from the tRPC call.
Additionally, I pass down the setItems() method to my form component and call that on the success of the mutation:
const [input, setInput] = useState<string>("");
const addItem = trpc.shoppingItem.addItem.useMutation({
async onSuccess(newItem) {
setItems((prev) => [...prev, newItem]);
console.log(newItem);
},
});
const handleAddItem = () => {
addItem.mutate({ name: input });
setModalOpen(false);
};
Related
im trying to build a weather app and i have been defined buttons to change the city name state and fetch the new city weather info by changing the dependency list of the useEffect hook and console log it for checking frst. but the problem is that the state is updating two steps behind the buttons that i click.
Main component for states and fetching data:
function App() {
const [cityName, setCityName] = useState('Toronto')
const [weather, setWeather] = useState('')
const url = `https://api.openweathermap.org/data/2.5/weather?q=${cityName}&units=metric&appid=5476766e86450fff39da1502218a3376`
const getData = () => {
axios.get(url).then((res) => {
setWeather(res.data);
});
console.log(weather)
}
useEffect(()=>{
getData()
},[])
return <div className="bg-slate-600">
<Topbuttons setCityName={setCityName} getData={getData} />
<Inputs setCityName={setCityName} getData={getData} />
<Data weather={weather} />
</div>;
}
export default App;
Im sending the setCity to the buttons component as a prop.
nevermind the getData() being repeated in useEffect its just for test
buttons component:
function Topbuttons({setCityName,getData}) {
const cities = ['Tehran', 'Paris', 'Newyork', 'Berlin', 'London']
return (
<div className='flex mx-auto py-3 justify-around space-x-8 items-center text-white text-l font-bold'>
{cities.map((v,i) => {
return <button
key={i}
onClick={()=>{
console.log(v)
setCityName(v)
getData()
}}>
{v}
</button>
})}
</div>
)
}
export default Topbuttons
thanks for your help
The problem is that you run setCityName() immediately on click but you run setWeather() after the promise is complete with data. One way to sync them is to run them at the same time after the async promise completes.
Here's a working sandbox of your code. Try using this setup:
app.js
import axios from "axios";
import { useEffect, useState } from "react";
import TopButtons from "./TopButtons";
function App() {
const [cityName, setCityName] = useState("Toronto");
const [weather, setWeather] = useState("");
const getData = (newCityName) => {
console.log("getData newCityName", newCityName);
const url = `https://api.openweathermap.org/data/2.5/weather?q=${newCityName}&units=metric&appid=5476766e86450fff39da1502218a3376`;
axios.get(url).then((res) => {
setCityName(newCityName);
setWeather(res.data);
});
};
useEffect(() => {
getData();
}, []);
return (
<div className="bg-slate-600">
<TopButtons setCityName={setCityName} getData={getData} />
<span>{JSON.stringify(weather)}</span>
</div>
);
}
export default App;
TopButtons.js
function Topbuttons({ getData }) {
const onClick = (newCityName) => getData(newCityName);
const cities = ["Tehran", "Paris", "Newyork", "Berlin", "London"];
return (
<div className="flex mx-auto py-3 justify-around space-x-8 items-center text-white text-l font-bold">
{cities.map((v, i) => {
return (
<button key={i} onClick={() => onClick(v)}>
{v}
</button>
);
})}
</div>
);
}
export default Topbuttons;
The next step would be to detect for a loading state. Since you're using Axios + React, I recommend using the axios-hooks library which uses Axios under-the-hood but provides loading state in hook-format which makes API calls easier to deal with.
I am trying to create a history page with react hooks that keeps track of the users most recent searches they don't have to be persistent through refreshes only from this session.
my search component looks like this this is a simple app that does not need a UI just a simple navigation on the search page it will show the results and on the history page I would like to be able to show the previous searches from this session
I am trying to keep track of the debouncedTerm so I can display it in a new component
import React, { useState, useEffect } from 'react';
import axios from 'axios';
const Search = () => {
const history = [];
const [term, setTerm] = useState('');
const [debouncedTerm, setDebouncedTerm] = useState(term);
const [results, setResults] = useState([]);
useEffect(() => {
const timerId = setTimeout(() => {
setDebouncedTerm(term);
}, 1000);
return () => {
clearTimeout(timerId);
};
}, [term]);
useEffect(() => {
const search = async () => {
const { data } = await axios.get('http://hn.algolia.com/api/v1/search?', {
params: {
query: debouncedTerm,
},
});
setResults(data.hits);
};
if (debouncedTerm) {
search();
}
}, [debouncedTerm]);
const renderedResults = results.map((result) => {
return (
<div key={result.objectID} className="item">
<div className="right floated content">
<a className="ui button" href={result.url}>
Go
</a>
</div>
<div className="content">
<div className="header">{result.title}</div>
</div>
</div>
);
});
return (
<div>
<div className="ui form">
<div className="field">
<label>Hacker News Search:</label>
<input
value={term}
onChange={(e) => setTerm(e.target.value)}
className="input"
/>
</div>
</div>
<div className="ui celled list">{renderedResults}</div>
</div>
);
};
export default Search;
Your code looks like it's going in the right direction but you have a constant const history = []; and you must keep in mind that this will not work, because you will have a new constant re-declared in every render. You must use React setState like so:
const [history, setHistory] = useState([]);
You can read more about it in the React documentation.
edit:
In order to add new elements to the existing history you have to append it like this:
setHistory(oldHistory => [...oldHistory, newHistoryElement]);
I've been banging my head against my desk for the past hour trying to figure out why the following code only displays the loading spinner and never updates to show the actual data even when I can see the data logged in the console, so I know the data is actually being fetched.
What is supposed to happen is that the page will display a spinner loader the first time the data is retrieved and then after useSWR has finished getting the data, the page will re-render to show the data. The data will eventually be shown in like a globe thing, but just as a prototype, I'm rendering it just with .map. The page is also not supposed to show the spinner a second time.
I think my problem might have something to do with calling the function to update the hook at the very end causes the whole page to re-render, although I'm not sure and if that is the case, I couldn't figure out how I'd make that work considering how SWR spontaneously re-fetches the data.
Here's the primary file:
import { useEffect, useState } from "react";
import Navbar from "#components/navbar";
import Globe from "#/components/interest-globes/globe";
import usePercentages from "#/components/interest-globes/globe-percentages";
const globeName = "primary";
const App = () => {
const [globePercentages, setGlobePercentages] = useState([]);
const [isFirstLoad, setFirstLoad] = useState(false);
const [isLoading, setLoading] = useState(false);
const [isError, setError] = useState();
useEffect(() => {
setFirstLoad(true);
}, []);
let {
globePercentages: newGlobePercentages,
isLoading: newLoadingState,
isError: newErrorState,
} = usePercentages(globeName);
console.log(newGlobePercentages, newLoadingState, newErrorState);
useEffect(() => {
updateGlobe();
}, [newGlobePercentages, newLoadingState, newErrorState]);
const updateGlobe = () => {
if (
isFirstLoad &&
(newGlobePercentages !== null || newGlobePercentages !== "undefined")
) {
setFirstLoad(false);
} else if (newLoadingState || !newGlobePercentages) {
setLoading(true);
return;
} else if (newErrorState) {
setError(newErrorState);
return;
}
setGlobePercentages(newGlobePercentages);
};
return (
<div>
<Navbar />
<div className="container-md">
<h1>The percentages are:</h1>
<br />
<Globe
globePercentages={globePercentages}
isLoading={isLoading}
isError={isError}
/>
{/* <Globe props={{ globePercentages, isLoading, isError }} /> */}
<br />
</div>
</div>
);
};
export default App;
Here is the globe component:
import Loading from "#components/loading";
import Error from "#components/error";
const Globe = ({ globePercentages, isLoading, isError }) => {
// const Globe = ({ props }) => {
// let { globePercentages, isLoading, isError } = props;
if (isLoading) {
return <Loading/>
}
else if (isError) {
return <Error/>
}
return (
<div>
{globePercentages.map((interest) => (
<div key={interest.name}>
<h2>Name: {interest.name}</h2>
<h4>Globe Percentage: {interest.globePercentage}%</h4>
<br />
</div>
))}
</div>
);
};
export default Globe;
Here is use-percentages:
import { useEffect } from "react";
import useSWR from "swr";
import fetcher from "#components/fetcher";
const usePercentages = (globeName) => {
const url = `/api/v1/interest-globes/${globeName}/get-globe-percentages`;
let { data, error } = useSWR(url, fetcher, { refreshInterval: 1000 });
return {
globePercentages: data,
isLoading: !error && !data,
isError: error,
};
};
export default usePercentages;
If you move your hook into your Globe component, everything becomes so simple:
const App = () => {
return (
<div>
<Navbar />
<div className="container-md">
<h1>The percentages are:</h1>
<br />
<Globe globeName={'primary'}/>
<br />
</div>
</div>
);
};
export default App;
const Globe = ({globeName}) => {
const lastPercentages = useRef(null);
const {
globePercentages,
isLoading,
isError,
} = usePercentages(globeName);
// only show the loader if isLoading AND lastPercentages is false-y
if (isLoading && !lastPercentages.current) {
return <Loading/>
}
if (isError) {
return <Error/>
}
lastPercentages.current = globePercentages ?? lastPercentages.current;
return (
<div>
{lastPercentages.current.map((interest) => (
<div key={interest.name}>
<h2>Name: {interest.name}</h2>
<h4>Globe Percentage: {interest.globePercentage}%</h4>
<br />
</div>
))}
</div>
);
}
The source of truth of globePercentages, isLoading, and isError come from the usePercentages hook. It feels so complicated because you're duplicating these pieces of data into new state variables, and then requiring that you keep them in sync. They are meant to be used as sources of truth, don't recreate them in your own state variables.
I am trying to get the spinner to load while waiting for API response.
I have tried several approaches but non seem to be working
I am using the react-spinner library
I set loading to true even before the API call, but still not showing.
I know I have to set it to false once the API call is successful, but I don't know what I am doing wrong that is not making the spinner to work
import React, { useState, useEffect } from "react";
import AuthService from "../../../services/auth.service";
import UserService from "../../../services/user.service";
import { BarLoader } from 'react-spinners'
function StatusTransactions() {
const [transactions, fetchTransactions] = useState([]);
const [loading, setLoading] = useState(false);
const [message, setMessage] = useState("");
const currentUser = Service.getUser();
useEffect(() => {
const getTransactions = () => {
setLoading(true)
UserService.getPendingTransactions(currentUser.user._id).then(response => {
fetchTransactions(response.data);
console.log(response.data)
}, (error) => {
const _content =
(error.response && error.response.data) ||
error.message ||
error.toString();
fetchTransactions(_content);
}
).catch(e => {
});
};
getTransactions();
}, []);
return (
<div className="content">
<ul className="dashboard-box-list user-dasboard-box">
{
transactions ? <BarLoader loading={loading} /> :
transactions.map(transaction => {
return (
<li key={transaction._id}>
<div className="invoice-list-item">
<strong>Professional Plan</strong>
<ul>
<li><span className="unpaid">{transaction.transactionStatus}</span></li>
<li>{transaction.TransactionBin}</li>
<li>Date: 12/08/2019</li>
</ul>
</div>
{/* <!-- Buttons --> */}
<div className="buttons-to-right">
Finish Payment
</div>
</li>
)
})}
</ul>
</div>
);
}
export default StatusTransactions;
I guess you should set size prop here "" as shown in the package documntation
your transaction state starts with an empty array ; which has logical value of true ,so transactions ? <BarLoader loading={loading} /> : seems to always pass BarLoader.
I finally solve it with suggestions from contributors.
I guess the BarLoader is not ok.
I tried ClipLoader like this It works.
loading ? <ClipLoader loading={loading} size={150} /> :
transactions.map(transaction => {
return (
Im trying to display a very long list from .json file (2k+ nodes with multiple lines of text). Is there a way to set useState variable after list finishes rendering itself cause useEffect refused to work
import React from 'react';
import LongList from './LongList.json';
const LongList = () => {
const [isLoaded,setIsLoaded] = React.useState(false);
React.useEffect(() => {
setIsLoaded(true);
}, [setIsLoaded]);
return (
<div>
{LongList.map(element => (
<div key={element.text}>{element.text}</div>
))}
</div>
);
};
You can do something like that by checking the index of the current item:
{LongList.map((element, index) => (
<div key={element.text}>{element.text}</div>
if(index === LongList.length - 1) {
// it is loaded
}
))}
You're on the right track with useEffect. I believe part of the issue you're having is due to using setIsLoaded as the second argument to useEffect. Instead, use [], which tells React that your effect doesn’t depend on any values from props or state, so it never needs to re-run. More info in the React docs.
Here's an example, with a console log in the useEffect callback showing it's only run once.
const data = Array.from(Array(10001).keys());
const LongList = ({data}) => {
const containerRef = React.useRef(null);
const [height, setHeight] = React.useState(0);
React.useEffect(() => {
console.log('Height: ', containerRef.current.clientHeight);
setHeight(containerRef.current.clientHeight);
}, []);
return (
<div>
<div>Height: {height}</div>
<div ref={containerRef}>
{data.map(element => (
<div key={element}>{element}</div>
))}
</div>
</div>
);
};
ReactDOM.render(<LongList data={data} />, document.getElementById('app'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.0/umd/react-dom.production.min.js"></script>
<div id="app"></div>