Build a modern, interactive stocks
chart with Next JS
In this guide, we are going to leverage the polygon API and the capabilities of Next JS to create a dynamic stocks price tracker and chart
Adding a real-time stocks chart to your application could be beneficial to you while keeping your users engaged.
If you’re a developer looking to hone your skills, this tutorial could be a valuable resource. You’ll also learn how to incorporate charts into your application and other valuable tools like react query for fetching api data.
Let’s get started
Prerequisites
To follow along, be sure to have the following:
- Polygon API: We will be using the Polygon API to fetch the market chart data for stocks. The Polygon API has a free Demo plan accessible to all users with a 5 calls/min rate limit and a monthly cap of 10,000 calls. Sign up for a Polygon account and apply for the Demo plan to generate your API key.
- Node.js and npm: Node.js is a JavaScript runtime that allows you to run JavaScript on your server or your computer. npm is a package manager for Node.js. You can download both from the official Node.js website.
- Text Editor or IDE: You will need a text editor or an Integrated Development Environment (IDE) to write your code. Some popular choices include Sublime Text, Atom, Visual Studio Code, and PyCharm. I will be using Visual Studio Code as the IDE, which is smart, fast and customizable IDE available in the market.
Setting up your project
Create a new Next JS app using
npx create-next-app@latest stocks-app
Install other required libraries
This application uses several libraries:
- React-query: This is a powerful asynchronous state management library. It makes fetching, caching, synchronizing and updating server state in your web applications a breeze.
- Axios: A promise-based HTTP client for making requests to our API
- Recharts: A charting library built on React components, for visualizing our data.
To install these packages, navigate to your project directory terminal and run the following commands
yarn add @tanstack/react-query axios recharts
This will install the libraries and add them to your package.json file.
Creating the folder structure
Run the code below from your project directory to open up your project with vscode
code .
- We will create a components folder at the root of our project to store all our components. However, if this was a more robust application, we will have to create a mini-components folder in the different app routing folders labeled as _components and store only reusable components in the components folder.
- We will create a services folder which will contain functions responsible for interacting with external services such as APIs
- A constants folder to store constant variables
- Context folder will be created to store the different contexts of the app used to manage some states.
- Lib folder will be created to contain utility functions used across the application. These utilities might include custom hooks or third-party libraries that are used globally.
- Providers folder will contain providers from external libraries
import StocksChart from "@/components/StocksChart";
export default function Home() {
return (
<main className="flex min-h-screen max-w-5xl mx-auto flex-col items-center justify-between p-24">
<StocksChart />
</main>
);
}Next, open the page.ts file located in the app folder. Delete everything in the file. Type rafce and hit tab or enter.
const page = () => {
return (
<div>page</div>
)
}
export default pageThis is an extension in vs code that is really useful. The extension is called ES7+ React/Redux/React-Native snippets. Install it and you are good to go.
Now lets create a component in our components folder and name it StocksChart.tsx
Create and export a function as before
const page = () => {
return (
<div>page</div>
)
}
export default pageImport this file into your page.tsx file and add the following styles as shown below.
import StocksChart from "@/components/StocksChart";
export default function Home() {
return (
<main className="flex min-h-screen max-w-5xl mx-auto flex-col items-center justify-between p-24">
<StocksChart />
</main>
);
}Now lets create a provider for our react query. In the providers folder, create a provider.tsx file and paste in the code below.
"use client";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
function makeQueryClient() {
return new QueryClient({
defaultOptions: {
queries: {
staleTime: 60 * 1000,
},
},
});
}
let browserQueryClient: QueryClient | undefined = undefined;
function getQueryClient() {
if (typeof window === undefined) {
return makeQueryClient();
} else {
if (!browserQueryClient) browserQueryClient = makeQueryClient();
return browserQueryClient;
}
}
export default function Provider({ children }: { children: React.ReactNode }) {
const queryClient = getQueryClient();
return (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);
}In this file, we are have implemented advanced server side rendering. We are creating a queryClient and wrapping our application in a QueryClientProvider.
Now, Open your StocksChart.tsx file and make the api call with react query
"use client";
import { useAppContext } from "@/context/AppContext";
import { fetchStock, fetchStockDetails } from "@/services/api.service";
import { useQuery } from "@tanstack/react-query";
import React from "react";
export interface IStock {
t: string;
c: string;
}
import {
Card,
CardContent,
CardFooter,
CardHeader,
} from "@/components/ui/card";
import { stockIntervals } from "@/constants/config";
import { IntervalRadio } from "./IntervalRadio";
import StockSearch from "./StockSearch";
import { StockChart } from "./StockChart";
const StocksChart = () => {
const { selectedSymbol, currentStockInterval } = useAppContext();
const { data: stock, status: stocksStatus } = useQuery({
queryKey: ["stock", selectedSymbol, currentStockInterval.value],
queryFn: () => fetchStock(selectedSymbol, currentStockInterval),
select: (data) =>
data.results.map((result: IStock) => ({
date: result.t,
price: result.c,
})),
});
const { data: stockDetails, status: stockDetailsStatus } = useQuery({
queryKey: ["stockDetails", selectedSymbol],
queryFn: () => fetchStockDetails(selectedSymbol),
select: (data) => ({
name: data.results.name,
ticker: data.results.ticker,
marketCap: data.results.market_cap,
logo: data.results.branding.logo_url,
}),
});
return (
<div className="w-full h-full text-slate-400">
Stocks
</div>
);
};By default, Next Js components are server components but react query only works on the client. Therefore we add the “use client” directive at the top of the file.
Now let’s create functions to make external api calls.
Create a file, api.service.ts in the service folder and put in the following code
import axios from "axios"
import { StockIntervalProps } from "@/context/AppContext"
const POLYGON_API_URL = "https://api.polygon.io"
export const fetchStock = (ticker: string, interval: StockIntervalProps) => {
return axios.get(`${POLYGON_API_URL}/v2/aggs/ticker/${ticker}/range/${interval.multiplier}/${interval.timeSpan}/${interval.from}/${interval.to}?adjusted=true&sort=asc&apiKey=${process.env.NEXT_PUBLIC_POLYGON_API_KEY}`)
.then((response) => response.data)
.catch(error => console.log(error))
}
export const searchStocks = (keyword: string) => {
return axios.get(`${POLYGON_API_URL}/v3/reference/tickers?search=${keyword}&market=stocks&sort=ticker&active=true&limit=10&apiKey=${process.env.NEXT_PUBLIC_POLYGON_API_KEY}`)
.then((response) => response.data)
.catch(error => console.log(error))
}
export const fetchStockDetails = (symbol: string) => {
return axios.get(`${POLYGON_API_URL}/v3/reference/tickers/${symbol}?apiKey=${process.env.NEXT_PUBLIC_POLYGON_API_KEY}`)
.then((response) => response.data)
.catch(error => console.log(error))
}In the code above, we have defined our base API Url from polygon. From polygon API reference page we can copy the respective endpoint url for fetching a stock, searching stocks and fetching stock details.
Moreover, we need an API key from polygon for all these to work. Obtain an API key from polygon and paste in into your .env file as below
NEXT_PUBLIC_POLYGON_API_KEY=Now we can call these functions using react query to fetch the data in our StocksChart.tsx file
const { data: stock, status: stocksStatus } = useQuery({
queryKey: ["stock", selectedSymbol, currentStockInterval.value],
queryFn: () => fetchStock(selectedSymbol, currentStockInterval),
select: (data) =>
data.results.map((result: IStock) => ({
date: result.t,
price: result.c,
})),
});
const { data: stockDetails, status: stockDetailsStatus } = useQuery({
queryKey: ["stockDetails", selectedSymbol],
queryFn: () => fetchStockDetails(selectedSymbol),
select: (data) => ({
name: data.results.name,
ticker: data.results.ticker,
marketCap: data.results.market_cap,
logo: data.results.branding.logo_url,
}),
});Now lets create a an app context to store and manage our state.
In the context folder, create an AppContext.tsx file and paste in the following
"use client";
import { stockIntervals } from "@/constants/config";
import { createContext, useContext, useState } from "react";
export interface StockIntervalProps {
value: string;
label: string;
multiplier: number;
timeSpan: string;
from: number;
to: number;
}
export const AppContext = createContext({
selectedSymbol: "",
setSelectedSymbol: (symbol: string) => {},
currentStockInterval: {
value: "",
label: "",
multiplier: 1,
timeSpan: "",
from: 1,
to: 1,
},
setCurrentStockInterval: ({
value,
label,
multiplier,
timeSpan,
from,
to,
}: StockIntervalProps) => {},
stockSearchInput: "",
setStockSearchInput: (input: string) => {},
});
const AppContextProvider = ({ children }: { children: React.ReactNode }) => {
const [selectedSymbol, setSelectedSymbol] = useState("AAPL");
const [currentStockInterval, setCurrentStockInterval] = useState(
stockIntervals[0]
);
const [stockSearchInput, setStockSearchInput] = useState("");
const value = {
selectedSymbol,
setSelectedSymbol,
currentStockInterval,
setCurrentStockInterval,
stockSearchInput,
setStockSearchInput,
};
return <AppContext.Provider value={value}>{children}</AppContext.Provider>;
};
export default AppContextProvider;
export const useAppContext = () => {
return useContext(AppContext);
};As seen above, we have created states to manage our application which we passed on to our app through a context and also created an interface for the interval which we will create later in this tutorial.
Let’s import what we need from the context for our StocksChart.tsx file.
Now, we will create a card which will contain the chart and other components.
First, we will install shadcn ui, a component library which will enable us create ui components faster. Install shadcn ui by running the command
npx shadcn-ui@latest initAdd a card component by running the command below
npx shadcn-ui@latest add card<Card className="w-full h-full text-slate-400">
<CardHeader>
<div className=" flex justify-between">
<div className="flex items-center gap-4">
<StockSearch />
<h3 className=" text-slate-700 text-2xl font-bold">
{stockDetails?.ticker && stockDetails.ticker}
</h3>
</div>
<div className="flex items-center gap-4">
{stockIntervals.map((interval) => (
<IntervalRadio
key={interval.label}
interval={interval}
currentInterval={currentStockInterval.value}
type="stock"
/>
))}
</div>
</div>
</CardHeader>
<CardContent>
<StockChart data={stock} dataInterval={currentStockInterval.value} />
</CardContent>
{stockDetails && (
<CardFooter>
<div className=" flex gap-4 items-end w-full justify-between text-sm text-gray-400">
<div className=" flex items-baseline gap-2">
<img
className="h-5 w-auto"
// width={0}
// height={0}
alt={stockDetails?.name}
src={`${stockDetails?.logo}?apiKey=${process.env.NEXT_PUBLIC_POLYGON_API_KEY}`}
/>
<h3 className="font-semibold text-gray-500 max-w-60 truncate">
{stockDetails?.name}
</h3>
</div>
{stockDetails?.marketCap && (
<div className="flex gap-2 font-medium">
<h3>Market Cap</h3>
<h3 className="font-semibold text-gray-500">
${stockDetails?.marketCap.toLocaleString()}
</h3>
</div>
)}
</div>
</CardFooter>
)}
</Card>As shown in the code above create a StockChart.tsx component and pass in the data for the currently fetched stock as props.
In your StockChart.tsx file, import the necessary files from recharts which was installed earlier. Pass the data prop into the AreaChart data. The code can be seen below
"use client";
import {
XAxis,
YAxis,
CartesianGrid,
Tooltip,
AreaChart,
Area,
ResponsiveContainer,
} from "recharts";
interface StockProps {
date: string;
price: number;
}
export const StockChart = ({
data,
}: {
data: { date: string; price: string }[];
}) => {
return (
<>
<ResponsiveContainer width={"100%"} height={200}>
{data ? (
<AreaChart
data={data}
margin={{ top: 10, right: 30, left: 0, bottom: 0 }}
className=" bg-[#82ca9e0d]"
>
<defs>
<linearGradient id="colorUv" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#8884d8" stopOpacity={0.8} />
<stop offset="95%" stopColor="#8884d8" stopOpacity={0} />
</linearGradient>
<linearGradient id="colorPrice" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#82ca9d" stopOpacity={0.8} />
<stop offset="95%" stopColor="#82ca9d" stopOpacity={0} />
</linearGradient>
</defs>
<XAxis
dataKey="date"
tickLine={false}
minTickGap={35}
fontSize={12}
/>
<YAxis
domain={["dataMin", "auto"]}
tickLine={false}
fontSize={10}
/>
<CartesianGrid strokeDasharray="3 3" />
<Tooltip
wrapperStyle={{ backgroundColor: "#a32828", borderRadius: 40 }}
formatter={formatP}
/>
{/* <Area type="monotone" dataKey="uv" stroke="#8884d8" fillOpacity={1} fill="url(#colorUv)" /> */}
<Area
type="monotone"
dataKey="price"
stroke="#82ca9d"
fillOpacity={1}
fill="url(#colorPrice)"
strokeWidth={1}
// unit={"usd"}
/>
</AreaChart>
) : (
<div className="flex flex-col space-y-3">
<Skeleton className="h-[125px] w-full rounded-xl" />
<div className="space-y-2">
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-full" />
</div>
</div>
)}
</ResponsiveContainer>
</>
);
};However, we need to format our data which include the price and date. let’s create a formatPrice function in our lib/utils.tsx file
export const formatPrice = (price: number) => {
return new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
minimumFractionDigits: 2
}).format(price)
}Create a function to format price which is located at the Y axis in the StockChart.tsx file
const formatYAxis = (item: any) => {
return `${formatPrice(item)}`;
};Create a function also to format the date with the date-fns library. Install this library
yarn add date-fnsCreate a function to format the dates.
import { format } from "date-fns";
const formatXAxis = (tickItem: any) => {
return format(tickItem, "p");
};Add these functions to the XAxis and YAxis elements of the recharts chart respectively. Below is the complete code for the StocksChart.tsx
"use client";
import {
XAxis,
YAxis,
CartesianGrid,
Tooltip,
AreaChart,
Area,
ResponsiveContainer,
} from "recharts";
import { format } from "date-fns";
import { formatPrice } from "@/lib/utils";
import { Skeleton } from "@/components/ui/skeleton";
interface StockProps {
date: string;
price: number;
}
export const StockChart = ({
data,
}: {
data: { date: string; price: string }[];
}) => {
const formatXAxis = (tickItem: any) => {
return format(tickItem, "p");
};
const formatYAxis = (item: any) => {
return `${formatPrice(item)}`;
};
const formatP = (value: any) => {
return formatPrice(value);
};
return (
<>
<ResponsiveContainer width={"100%"} height={200}>
{data ? (
<AreaChart
data={data}
margin={{ top: 10, right: 30, left: 0, bottom: 0 }}
className=" bg-[#82ca9e0d]"
>
<defs>
<linearGradient id="colorUv" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#8884d8" stopOpacity={0.8} />
<stop offset="95%" stopColor="#8884d8" stopOpacity={0} />
</linearGradient>
<linearGradient id="colorPrice" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#82ca9d" stopOpacity={0.8} />
<stop offset="95%" stopColor="#82ca9d" stopOpacity={0} />
</linearGradient>
</defs>
<XAxis
dataKey="date"
tickFormatter={formatXAxis}
tickLine={false}
minTickGap={35}
fontSize={12}
/>
<YAxis
domain={["dataMin", "auto"]}
tickLine={false}
fontSize={10}
tickFormatter={formatYAxis}
/>
<CartesianGrid strokeDasharray="3 3" />
<Tooltip
wrapperStyle={{ backgroundColor: "#a32828", borderRadius: 40 }}
formatter={formatP}
/>
{/* <Area type="monotone" dataKey="uv" stroke="#8884d8" fillOpacity={1} fill="url(#colorUv)" /> */}
<Area
type="monotone"
dataKey="price"
stroke="#82ca9d"
fillOpacity={1}
fill="url(#colorPrice)"
strokeWidth={1}
// unit={"usd"}
/>
</AreaChart>
) : (
<div className="flex flex-col space-y-3">
<Skeleton className="h-[125px] w-full rounded-xl" />
<div className="space-y-2">
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-full" />
</div>
</div>
)}
</ResponsiveContainer>
</>
);
};Create a StocksSearch.tsx component to search for stocks and paste the code below
import { useState } from "react";
import { HiX, HiSearch } from "react-icons/hi";
import { useQuery } from "@tanstack/react-query";
import { searchStocks } from "@/services/api.service";
import { useAppContext } from "@/context/AppContext";
import SearchResults from "./SearchResults";
const StockSearch = () => {
const {stockSearchInput, setStockSearchInput} = useAppContext()
const { data} = useQuery({
queryKey: ["stockSearchResult", stockSearchInput],
queryFn: () => searchStocks(stockSearchInput),
refetchInterval: 3 * 60 * 1000,
staleTime: 3 * 60 * 1000,
enabled: !!stockSearchInput
});
const clear = () => {
setStockSearchInput("");
};
return (
<div className="flex items-center my-4 border-2 rounded-md w-64 relative z-50 bg-white border-neutral-200">
<input
type="text"
value={stockSearchInput}
onChange={(e) => {
setStockSearchInput(e.target.value);
}}
className="w-full px-4 py- focus:outline-none rounded-md"
placeholder="Search stocks..."
onKeyUp={(event) => {
if (event.key === "Enter") {
updateMatches();
}
}}
/>
{stockSearchInput && (
<button className="text-gray-600" onClick={clear}>
<HiX size={14} />
</button>
)}
<button
onClick={updateMatches}
className="p-2 bg-primary-400 text-primary-50 rounded-md flex justify-center items-center m-1"
>
<HiSearch size={20} />
</button>
{stockSearchInput && data?.results.length > 0 ? <SearchResults results={data.results} /> : null}
</div>
);
};
export default StockSearch;In the code above, an input element is created and on input change, the state is updated. However, the state is used as a query Key for reactQuery which refetches data whenever its query key changes.
This way, whenever the search input changes, a new list of stocks is fetched and the stocks list is updated.
Next, we will create an interval radio component for switching between intervals for fetching stock history data.
Create a stocksInterval variable as below in a config.tsx file in the constants folder
import { pastMonth, pastWeek, pastYear, today } from "@/lib/date-helper";
export const stockIntervals = [
{ label: "1W", value: "1W", multiplier: 4, timeSpan: "hour", from: pastWeek(), to: today() },
{ label: "1M", value: "1M", multiplier: 1, timeSpan: "day", from: pastMonth(), to: today() },
{ label: "1Y", value: "1Y", multiplier: 1, timeSpan: "week", from: pastYear(), to: today() },
];Create a date-helper file in the lib folder
export const pastWeek = () => {
return new Date(new Date().getTime() - 7 * 24 * 60 * 60 * 1000).getTime()
}
export const pastMonth = () => {
return new Date(new Date().getTime() - 30 * 24 * 60 * 60 * 1000).getTime()
}
export const pastYear = () => {
return new Date(new Date().getTime() - 12 * 30 * 24 * 60 * 60 * 1000).getTime()
}
export const today = () => {
return new Date().getTime()
}Import the stocksInterval.ts file into the intervalRadio.tsx file
import { stockIntervals } from "@/constants/config";
import { useAppContext } from "@/context/AppContext";
interface IProps {
interval: { label: string; value: number | string };
currentInterval: number | string;
type?: string;
setCurrentInterval?: any;
}
export const IntervalRadio = ({
interval,
currentInterval,
type,
setCurrentInterval,
}: IProps) => {
const { setCurrentStockInterval } = useAppContext();
const handleRadioChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const stockInt = stockIntervals.find(
(stockInterval) => stockInterval.value === event.target.value
);
stockInt && setCurrentStockInterval(stockInt);
};
const checked = currentInterval === interval.value;
return (
<label
htmlFor=""
className={`rounded-md relative transition duration-300 border-slate-400 border select-none cursor-pointer aspect-square w-8 flex items-center justify-center ${
checked && "bg-primary text-white"
}`}
>
<input
checked={checked}
onChange={handleRadioChange}
value={interval.value}
type="radio"
className="absolute opacity-0 cursor-pointer top-0 left-0 right-0 bottom-0"
/>
<span className=" text-sm">{interval.label}</span>
</label>
);
};In the code above, when an interval is checked, the currentStockInterval state changes which triggers a refetch with reactQuery in the StocksChart.tsx file. Hence, new set of history data is received after an API call with the fetchStock function. This data become the new stock historyData and is used to update the chart.
const { data: stock, status: stocksStatus } = useQuery({
queryKey: ["stock", selectedSymbol, currentStockInterval.value],
queryFn: () => fetchStock(selectedSymbol, currentStockInterval),
select: (data) =>
data.results.map((result: IStock) => ({
date: result.t,
price: result.c,
})),
});Now everything is set and you can run yarn dev to view the result


