Reputation: 515
My objective is to build a page using nextjs 14 that is a stock scanner that loads some data from an external api using some default parameters. In addition, I want to provide the user some additional options to customize paramaters and re-run the scan and load the results (e.g, below).
The data is loading in fine and I can actually load in new data with a server action, but there is one problem. As it is, the line
setIsLoading(true
in my reload function leads to the dreaded async/await error
Unhandled Runtime Error
Error: async/await is not yet supported in Client Components, only Server Components. This error is often caused by accidentally adding 'use client'
to a module that was originally written for the server.
Is this a reasonable approach to organizing this client/server component interaction with server actions. If so, what is the proper way to update the isLoading
state so that I can provide some feedback to the user when the scan button is clicked?
here is my page.tsx
"use client";
import { BollingerBandsScreenerResult } from "../../screener-types";
import { ScreenerErrorMsg } from "@/app/services/ScreenerService";
import { Either, match } from "@/app/utils/BasicUtils";
import NavWrapper from "@/app/components/NavWrapper";
import TableSkeleton from "@/app/components/screener/TableSkeleton";
import BollingerBandsBreachHeader from "../breach/BollingerBandsBreachHeader";
import SimpleScreenerResultsTable from "@/app/components/screener/SimpleScreenerResultsTable";
import BollingerBandsBreachParameters from "./BollingerBreachParameters";
import { getBBBreachData } from "./actions";
import { useEffect, useState } from "react";
const BollingerBreachScreen = () => {
const [isLoading, setIsLoading] = useState(true);
const [data, setData] = useState<Either<
ScreenerErrorMsg,
BollingerBandsScreenerResult[]
> | null>(null);
useEffect(() => {
const fetchData = async () => {
const data = await getBBBreachData();
setIsLoading(false);
setData(data);
};
fetchData();
}, []);
const reloadData = async (
minClosePrice: number,
period: number,
multiplier: number,
lookback: number
) => {
setIsLoading(true); //this is the problem line
const reload = await getBBBreachData(
minClosePrice,
period,
multiplier,
lookback
);
setData(reload);
};
if (!data || isLoading) {
return (
<NavWrapper>
<div className="h-full w-5/6 mx-auto text-black">
<BollingerBandsBreachHeader />
<TableSkeleton />
</div>
</NavWrapper>
);
}
return match(
data,
(errorString) => {
return (
<NavWrapper>
<div className="h-full w-5/6 mx-auto text-black">
<BollingerBandsBreachHeader />
<BollingerBandsBreachParameters updateFn={reloadData} />
<div>{errorString}</div>
</div>
</NavWrapper>
);
},
(bbResults) => {
return (
<NavWrapper>
<div className="h-full w-5/6 mx-auto text-black">
<BollingerBandsBreachHeader />
<BollingerBandsBreachParameters updateFn={reloadData} />
<SimpleScreenerResultsTable results={bbResults} />
</div>
</NavWrapper>
);
}
);
};
export default BollingerBreachScreen;
Here is my component for user interaction
"use client";
import { Roboto } from "next/font/google";
import { useState } from "react";
const roboto = Roboto({
subsets: ["latin"],
weight: ["100", "300", "400", "500", "700"],
});
interface BollingerBandsBreachParametersProps {
updateFn: (
minClosePrice: number,
period: number,
multiplier: number,
lookback: number
) => void;
}
const BollingerBandsBreachParameters: React.FC<
BollingerBandsBreachParametersProps
> = ({ updateFn }) => {
const [minClosePrice, setMinClosePrice] = useState<number>(10);
const [period, setPeriod] = useState<number>(20);
const [multiplier, setMultiplier] = useState<number>(2);
const [lookback, setLookback] = useState<number>(1);
const handlePeriodSet = (event: React.ChangeEvent<HTMLInputElement>) => {
const minValue = parseInt(event.target.value);
setPeriod(isNaN(minValue) ? 0 : minValue);
};
const handleMultiplierSet = (event: React.ChangeEvent<HTMLInputElement>) => {
const minValue = parseFloat(event.target.value);
setMultiplier(isNaN(minValue) ? 0 : minValue);
};
const handleLookbackSet = (event: React.ChangeEvent<HTMLInputElement>) => {
const minValue = parseInt(event.target.value);
setLookback(isNaN(minValue) ? 0 : minValue);
};
const handleMinCloseSet = (event: React.ChangeEvent<HTMLInputElement>) => {
const minValue = parseInt(event.target.value);
setMinClosePrice(isNaN(minValue) ? 0 : minValue);
};
const handleClick = async () => {
updateFn(minClosePrice, period, multiplier, lookback);
};
return (
<>
<div className="font-semibold text-lg mb-2">Scan Criteria</div>
<div className="flex items-center space-x-2 mt-1 mb-4 ">
<div>Period</div>
<input
type="number"
className="w-16 p-2 border border-gray-300 text-xs"
placeholder="Min"
value={period}
id="period"
name="period"
required
onChange={handlePeriodSet}
/>
<div>Multiplier</div>
<input
type="number"
className="w-16 p-2 border border-gray-300 text-xs"
placeholder="2"
id="multiplier"
name="multiplier"
required
value={multiplier}
onChange={handleMultiplierSet}
/>
<div>Lookback</div>
<input
type="number"
className="w-16 p-2 border border-gray-300 text-xs"
placeholder="2"
id="lookback"
name="lookback"
required
value={lookback}
onChange={handleLookbackSet}
/>
<div>Minimum Close Price</div>
<input
type="number"
className="w-16 p-2 border border-gray-300 text-xs"
placeholder="10"
id="minClosePrice"
name="minClosePrice"
required
value={minClosePrice}
onChange={handleMinCloseSet}
/>
<button
className="bg-transparent hover:bg-blue-500 text-blue-700 font-semibold hover:text-white py-1 px-3 border border-blue-500 hover:border-transparent rounded"
onClick={handleClick}
>
Scan
</button>
</div>
</>
);
};
export default BollingerBandsBreachParameters;
Here is my server action for loading data in actions.ts
export async function getBBBreachData(
prevState: any,
minClosePrice: number = 10,
period: number = 20,
multiplier: number = 2,
lookback: number = 1
): Promise<Either<ScreenerErrorMsg, BollingerBandsScreenerResult[]>> {
try {
const url = `${BOLLINGER_BASE}/lower-breach?minClosePrice=${minClosePrice}&period=${period}&multiplier=${multiplier}&lookback=${lookback}`;
const response = await fetch(url, {
next: { revalidate: 0 },
});
if (response.ok) {
const res = await response.json();
return Right(res);
} else {
return Left("Could not fetch launchpad results");
}
} catch (error) {
console.error(error);
return Left("Error fetching mg leader results");
}
}
Upvotes: 0
Views: 660
Reputation: 49571
if you are using action for the form submission you can use useFormStatus
useFormStatus is a Hook that gives you status information of the last form submission.
const { pending, data, method, action } = useFormStatus();
Be careful to the caveats
- The useFormStatus Hook must be called from a component that is rendered inside a .
- useFormStatus will only return status information for a parent . It will not return status information for any rendered in that same component or children components.
other wise you could use Suspense
lets you display a fallback until its children have finished loading.
<Suspense fallback={<Loading />}> <SomeComponent /> </Suspense>
Upvotes: 0