React Hooks have revolutionized the React ecosystem since their inception in React 16.8. They offer the ability to leverage state and lifecycle features inside functional components, which was previously only possible in class components.
Earlier on, we’ve written some deep dives into some of the core React Hooks like the useEffect, useContext and useReducer hooks.
Despite the utility and usefulness React Hooks provide us, improper use can lead to performance degradation and elusive bugs. In this article, we’ll outline some common mistakes developers make when using React Hooks and solutions on how best to avoid them.
It’s a common error to use hooks inside loops, conditional statements or nested functions. This can lead to inconsistent hook calls between renders, resulting in erratic behaviors and hard-to-find bugs.
function Component() {
if (condition) {
const [state, setState] = useState(initialState);
}
// ...
}
Hooks must always be invoked in the same order during every component render. This helps promotes more predictable and stable component behavior.
function Component() {
const [state, setState] = useState(initialState);
if (condition) {
// manipulate or use the state here.
}
// ...
}
Over-reliance on state is another frequent pitfall. Not every variable in a component needs to be part of component state, especially if it doesn’t trigger a re-render. By distinguishing between stateful and non-stateful data, we can optimize our component’s performance by reducing unnecessary re-renders.
// unneccessary if we don't need a re-render when value changes
const [value, setValue] = useState("");
// instead, we can just assign a variable
let value = "";
React state is inherently immutable, and direct mutation is an error sometimes made by developers.
const [state, setState] = useState([]);
state.push("new item"); // Incorrect!
Direct state mutation doesn’t immediately trigger a re-render, causing a discrepancy between the displayed state and the actual state of the component.
Instead, we should always use the state update function available to use from the useState
hook to update state. When state changes this way, it triggers a necessary re-render which ensures consistency between the component’s actual state and its rendered output.
const [state, setState] = useState([]);
setState((prevState) => [...prevState, "new item"]); // Correct!
A common pitfall when using React Hooks is the misuse of stale state data. This can occur when we directly reference the state variable in consecutive state updates. Because state updates may be asynchronous, the state variable might not reflect the latest value when it’s referenced in successive calls.
const [count, setCount] = useState(0);
setCount(count + 1);
setCount(count + 1); // Incorrect! `count` could be stale.
In the incorrect usage above, count
is referenced directly within each setCount()
call. If these state updates are batched (as they often are in event handlers and lifecycle methods), then both calls to setCount()
will use the same initial value of count
, leading to an incorrect final state.
Instead, we can use the updater function form of setCount()
, which ensures that each update is based on the latest state. The updater function takes the previous state as an argument and returns the new state, so each consecutive update will have the correct value, preventing stale state data issues.
const [count, setCount] = useState(0);
setCount((prevCount) => prevCount + 1);
setCount((prevCount) => prevCount + 1); // Correct! Using the updater function.
The useEffect
hook is often misused to run on every component update. Although this is necessary in some scenarios, often we only need to run an effect once on mount, or when specific dependencies change.
useEffect(() => {
fetch("https://example.com/api").then(/* ... */);
}); // Will run every render
In the example above, running the effect on every render will result in numerous API calls which could lead to performance degradation and inconsistent state.
By supplying an empty dependency array, we can ensure the effect runs only once on mount, similar to the traditional componentDidMount
lifecycle method in class components.
useEffect(() => {
fetch("https://example.com/api").then(/* ... */);
}, []); // Runs once on mount
Side effects are operations that interact with the outside of a function scope, affecting the external environment. This could involve data fetching, subscriptions, manual DOM manipulations, and timers like setTimeout
and setInterval
.
Failing to clear side effects in the useEffect
hook is another common error that can occur that can lead to unexpected behavior and/or memory leaks.
useEffect(() => {
const timer = setTimeout(() => {
// do something
}, 1000);
// Missing cleanup!
});
By returning a cleanup function from our effect, we can remove side effects before the component unmounts or before the effect runs again, preventing memory leaks.
useEffect(() => {
const timer = setTimeout(() => {
// do something
}, 1000);
// cleanup function to remove the side effect
return () => {
clearTimeout(timer);
};
});
A common mistake is neglecting to include dependencies in the dependency array of useEffect
, useCallback
or useMemo
hooks. If a variable from the component scope is used within these hooks’ callbacks, it should often be included within the dependency array.
function Component({ prop }) {
useEffect(() => {
console.log(prop);
}, []); // Missing dependency: 'prop'
// ...
}
Omitting dependencies can lead to the capture of stale variables that may have changed since the effect was last run, which can lead to unpredictable behavior.
function Component({ prop }) {
useEffect(() => {
console.log(prop);
}, [prop]); // Correct dependency array
// ...
}
By correctly declaring all dependencies, we ensure that the hook updates whenever a dependency changes. This results in consistent and expected behavior.
Memoization is an optimization technique that primarily speeds up applications by storing the results of expensive function calls and reusing the cached result when the same inputs occur again. This technique is incredibly useful for functions that are computationally intensive and are frequently called with the same arguments.
React provides two hooks, useMemo
and useCallback
, that implement memoization. useMemo
is used for memoizing the return value of a function, while useCallback
is used for memoizing the instance of the function itself.
If expensive functions are not correctly memoized, they can provoke unnecessary re-renders. In the following example, if the parent component re-renders for any reason, expensiveFunction()
will be recreated. This will cause unnecessary re-renders in the child component, as it will receive a new prop each time, regardless of whether the actual computation result has changed.
function Component({ prop }) {
const expensiveFunction = () => {
// Expensive computation
};
return <ChildComponent func={expensiveFunction} />;
}
We can optimize this by using the useCallback
hook:
function Component({ prop }) {
const expensiveFunction = useCallback(
() => {
// Expensive computation
},
[
/* dependencies */
]
);
return <OtherComponent func={expensiveFunction} />;
}
In this correct usage, the useCallback
hook ensures that expensiveFunction()
is only recreated when its dependencies change. This means that the child component will only re-render when the computed value changes, preventing unnecessary re-renders and enhancing the performance of the React application.
React Hooks have significantly transformed the React landscape by allowing the use of state and lifecycle features in functional components. However, they can also present unique challenges.
Errors such as using hooks within conditional statements, overusing state, mutating state directly, misusing stale state data, etc. can lead to inefficient code and difficult-to-trace bugs. By avoiding these common mistakes and following best practices, developers can fully harness the potential of React Hooks, leading to more performant, stable and maintainable React applications.
]]>Next.js is a full-stack React framework that offers many great features, such as static site generation, server-side rendering, file-system routing and more. In version 13.4, Next.js released a stable version of the App Router, which changes how developers can build their applications.
The App Router introduced a new way of creating pages with nested routes and layouts. It offers simplified data fetching, streaming and React Server Components as a first-class citizen. It’s clear that App Router is the direction where Next.js is heading, so let’s have a look at how to migrate to it from the Pages directory.
Next.js makes it very easy to create new pages in an application. It uses the file-system as an API and creates routes for every file in the pages directory. Let’s take the following folder structure as an example:
Pages—directory structure
src/
└── pages/
├── index.js
├── about.js
└── contact/
└── index.js
For these files, Next.js would create these routes:
It’s a bit different when using the App Router. All the code for the App Router needs to be placed inside the app
directory. Here’s how we would create the aforementioned pages with the App Router.
App Router—directory structure
src/
└── app/
├── page.js
├── about/
│ └── page.js
└── contact/
└── page.js
Every page component needs to be called page
, and its extension can be either js
, jsx
or tsx
. Similarly to the Pages directory, the folder names are segments used to create route names, but the component files always need to be called page
.
There are situations when we might not know the exact name of a URL segment. For instance, we could have a page that should display the user’s profile based on the user’s ID in the URL. That’s where dynamic routes come into play. In the Pages directory, this can be achieved by wrapping a folder’s or file’s name with brackets: [id]
or [slug]
.
Pages—dynamic routes
src/
└── pages/
├── user/
│ └── [id].js
└── article/
└── [slug]/
└── index.js
To get access to the user’s ID from the URL in the Pages directory component, we need to use the Next.js router, as shown below:
Pages—accessing URL params
import { useRouter } from "next/router";
const UserProfile = props => {
const router = useRouter();
return <div>User ID: {router.query.id}</div>;
};
export default UserProfile;
And here’s how we can achieve the same thing with the App Router.
App Router—dynamic routes
src/
└── app/
├── article/
│ └── [slug]/
│ └── page.js
└── user/
└── [id]/
└── page.js
Since every page component needs to be called page
, we need to add brackets in the folder names. The code below shows how we can access the dynamic URL params:
App Router—accessing URL params
const User = props => {
const { params, searchParams } = props;
return <div>User ID: {params.id}</div>;
};
export default User;
We no longer need to use the useRouter
hook, as params
and searchParams
are passed via props.
A very important thing to note is that every component in the App Router is, by default, a React Server Component (RSC). This means that the components are rendered on the server only. There is no hydration process on the client side, and any JavaScript code related to the server component is skipped and is not sent to the client. If you don’t know what React Server Components are, check out this page.
If you would like to change a component from RSC to a client component, you can do so by adding the "use client"
directive at the top of the file.
"use client"
const User = props => {
const { params, searchParams } = props;
return <div>User ID: {params.id}</div>;
};
export default User;
Next.js can generate static pages at build time or dynamic pages at runtime on the server. In the pages directory, there were three main approaches to fetching data before generating pages—getStaticProps
with or without revalidation and getServerSideProps
. getStaticProps
is called when the application is built with the next build
command, while the latter is called at runtime when a user makes a request to see a page.
However, it’s possible to return the revalidate
property from getStaticProps
to turn a page from static site generation (SSG) to incremental static regeneration (ISR). The difference is that instead of generating the page at build time, the page is generated at runtime, just like with getServerSideProps
. The page is regenerated based on the revalidate
property. Let’s have a look at how we can migrate these to the App Router.
The example below shows how to use getStaticProps
to fetch and display a list of posts.
Pages—getStaticProps (SSG)
export const getStaticProps = async () => {
const response = await fetch("https://jsonplaceholder.typicode.com/posts");
return {
props: {
posts: await response.json(),
},
/*
With the revalidate property, the component will use ISR; without it, it will be just SSG.
*/
revalidate: 60
};
};
const Articles = props => {
const { posts } = props;
return (
<div>
<h1>Articles</h1>
<ul>
{posts.map(post => {
return <li key={post.id}>{post.title}</li>;
})}
</ul>
</div>
);
};
export default Articles;
Pages—getServerSideProps
export const getServerSideProps = async context => {
const response = await fetch(
`https://jsonplaceholder.typicode.com/users/${context.params.id}`
);
return {
props: {
user: await response.json(),
},
};
};
const UserProfile = props => {
const { user } = props;
return (
<div>
<div>User ID: {user.id}</div>
<div> Name: {user.name}</div>
</div>
);
};
export default UserProfile;
In the App Router, we don’t use getStaticProps
and getServerSideProps
. Instead, all data fetching needs to be done in Server Components.
Normal React components have to be synchronous, but React Server Components can be asynchronous. Therefore, we use async/await
to perform API requests inside of components and then use the fetched data to return JSX markup.
Below, we have an example of how to implement an equivalent of getStaticProps
.
App Router—getStaticProps equivalent
const Articles = async props => {
const response = await fetch("https://jsonplaceholder.typicode.com/posts", {
cache: "force-cache",
});
const posts = await response.json();
return (
<div>
<h1>Articles</h1>
<ul>
{posts.map(post => {
return <li key={post.id}>{post.title}</li>;
})}
</ul>
</div>
);
};
export default Articles;
Inside of the Articles
component, we make an API request using fetch
. The key part here is the cache: "force-cache"
value. Next.js extends the native Fetch
API to enhance the functionality of server components. In this scenario, the data returned from the API will be cached.
It’s worth noting that all requests performed using fetch
that do not specify the cache
value will be cached by default. Therefore, cache: "force-cache"
, can actually be omitted.
In a situation when data should be revalidated, we can specify the next.revalidate
option and set it to a number of seconds. The code below shows how to revalidate cached data every 30 seconds.
App Router—getStaticProps with revalidate (ISR)
const response = await fetch("https://jsonplaceholder.typicode.com/posts", {
next: {
revalidate: 30
}
});
Last but not least, we have getServerSideProps
equivalent.
App Router—getServerSideProps equivalent
const User = async props => {
const { params, searchParams } = props;
const response = await fetch(
`https://jsonplaceholder.typicode.com/users/${params.id}`,
{
cache: "no-store",
}
);
const user = await response.json();
return (
<div>
<div>User ID: {user.id}</div>
<div> Name: {user.name}</div>
</div>
);
};
export default User;
The only difference from the getStaticProps
equivalent example is that instead of passing cache: "force-cache"
to the fetch
, it needs to be set to no-store
.
A Page component can export a function called getStaticPaths
to define dynamic paths for pages that should be generated at build time.
export async function getStaticPaths() {
return {
paths: [
{
params: {
slug: 'hello-app-router'
},
params: {
slug: 'migrating-from-next-pages'
}
}
]
}
}
export async function getStaticProps({ params }) {
const res = await fetch(`https://domain.com/articles/${params.slug}`)
return {
props: {
article: await res.json()
}
}
}
export default function Article(props) {
return <DisplayArticle article={props.article} />
}
When using the App Router, we need to call the function generateStaticParams
instead of getStaticPaths
.
App Router—getStaticPaths equivalent
export async function generateStaticParams() {
return {
paths: [
{
params: {
slug: 'hello-app-router'
},
params: {
slug: 'migrating-from-next-pages'
}
}
]
}
}
export default function Article(props) {
const res = await fetch(`https://domain.com/articles/${props.params.slug}`)
const article = await res.json()
return <DisplayArticle article={article} />
}
Creating API endpoints is a breeze in Next.js, as it uses the file system to generate API endpoints. To create an API endpoint in the Pages directory, we need to create a file that exports a handler in the src/pages/api
directory.
Pages—API route structure
src/
└── pages/
└── api/
└── articles.js
Pages—API route example
export default async function handler(req, res) {
const response = await fetch("https://jsonplaceholder.typicode.com/posts");
res.status(200).json(await response.json());
}
Creating API endpoints in App Router is quite similar but with a few differences. All API route files need to be named route.js
. Therefore, to achieve the same API Route as we just did with the Pages, we would need to create the route.js
file in the app/api/articles
folder.
App Router—route handler structure
src/
└── app/
└── api/
└── articles/
└── route.js
The route.js
file also needs to export a handler function, but there is an important difference. Instead of using the default
export to provide a handler function, the App Router API Routes need to export a function that are named like HTTP methods.
App Router—route handler example
import { NextResponse } from "next/server";
export async function GET(request) {
const response = await fetch("https://jsonplaceholder.typicode.com/posts");
return NextResponse.json({
posts: await response.json(),
});
}
Next.js supports the following HTTP Methods: GET
, POST
, PATCH
, DELETE
, HEAD
and OPTIONS
, and actually an API Route handler can export multiple handlers.
export async function GET(request) {}
export async function POST(request) {}
export async function PATCH(request) {}
export async function DELETE(request) {}
Migrating from the Pages directory to the App Router in Next.js offers enhanced route control, dynamic routes, API routes, flexible middleware handling and improved rendering strategies with SSG and SSR. By adopting React Server Components, we can optimize performance and interactivity. Upgrade your Next.js project today and enjoy the benefits of the App Router and React Server Components.
]]>GraphQL is a query language for making requests to APIs. With GraphQL, the client tells the server exactly what it needs and the server responds with the data that has been requested. In an earlier article, we went through an exercise to create a GraphQL server API with Apollo Server in Node.js.
In today’s article, we’ll spend some time seeing how we can get started with using GraphQL on the client and we’ll use React, a widely adopted library for building user interfaces that couples well with GraphQL.
In the realm of GraphQL, the client serves as the medium through which we interact with our GraphQL server. In addition to sending queries/mutations and receiving responses from the server, the client is also responsible for managing the cache, optimizing requests and updating the UI.
Though we can make a GraphQL HTTP request with a simple POST command, using a specialized GraphQL client library can make the development experience much easier by providing features and optimizations like caching, data synchronization, error handling and more.
Many popular GraphQL clients exist in the developer ecosystem today such as Apollo Client, URQL, Relay and React Query. In this article, we’ll leverage React Query as our GraphQL client library.
Assuming we have a running React application, we can begin by installing the @tanstack/react-query
, graphql-request
and the graphql
packages.
npm install @tanstack/react-query graphql-request graphql
@tanstack/react-query
is the React Query library we’ll use to make queries and mutations. The graphql-request
and graphql
libraries will allow us to make our request functions to our GraphQL server and provide the necessary utilities to parse our GraphQL queries.
To begin using React Query utilities within our app, we’ll need to first set up a QueryClient
and wrap our application’s root component within a QueryClientProvider
. This will enable all the child components of App
to access the QueryClient
instance and, therefore, be able to use React Query’s hooks and functionalities.
In our root index file where the parent <App />
component is being rendered, we’ll import QueryClient
and QueryClientProvider
from the tanstack/react-query
library.
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
QueryClient
is responsible for executing queries and managing their results and state, while QueryClientProvider
is a React context provider that allows us to pass the QueryClient
down our component tree.
We’ll then create a new instance of QueryClient
and pass it down as the value of the client
prop of QueryClientProvider
that we’ll wrap the root App
component with.
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import App from "./App";
// create the query client instance
const queryClient = new QueryClient();
const rootElement = document.getElementById("root");
const root = createRoot(rootElement);
// pass our query client down our app component tree
root.render(
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>
);
The main composition function, provided to us from React Query, to execute GraphQL queries is the useQuery function.
The useQuery()
hook takes a unique key and an asynchronous function that resolves the data returned from the API or throws an error. To see this in action, we’ll attempt to make a GraphQL query to the publicly accessible Star Wars GraphQL API.
We’ll create a component named App
and utilize the useQuery()
hook within it to retrieve a list of films from the allFilms
query field. First, we’ll construct the GraphQL query as follows:
import { gql } from "graphql-request";
const allFilms = gql/* GraphQL */ `
query allFilms($first: Int!) {
allFilms(first: $first) {
edges {
node {
id
title
}
}
}
}
`;
The GraphQL document above defines a query named allFilms
, which accepts a variable labeled $first
that limits the number of films retrieved from the API.
In the component function, we’ll leverage the useQuery()
hook to initialize the GraphQL request when the component mounts. We’ll supply a unique key for the query, 'fetchFilms'
, and an asynchronous function that triggers a request to the GraphQL endpoint.
import { gql, request } from "graphql-request";
import { useQuery } from "@tanstack/react-query";
const allFilms = gql/* GraphQL */ `
...
`;
const App = () => {
const { data } = useQuery({
queryKey: ["fetchFilms"],
queryFn: async () =>
request(
"https://swapi-graphql.netlify.app/.netlify/functions/index",
allFilms,
{ first: 10 }
),
});
};
In the above code snippet, the useQuery()
hook is triggered when the App
component mounts, invoking the GraphQL query with the specified unique key and variables.
The useQuery()
hook returns a result
object that contains various properties that represent the state and outcome of the query. In addition to containing the data
fetched from the query when successful, the result
object also contains isLoading
and isError
values. isLoading
tracks the loading status of the request and isError
helps notify us when an error has occurred during the request.
With the isLoading
and isError
values, we can have the component render different elements depending on the state of our GraphQL request.
import { gql, request } from "graphql-request";
import { useQuery } from "@tanstack/react-query";
const allFilms = gql/* GraphQL */ `
query allFilms($first: Int!) {
allFilms(first: $first) {
edges {
node {
id
title
}
}
}
}
`;
const App = () => {
const { data, isLoading, isError } = useQuery({
queryKey: ["fetchFilms"],
queryFn: async () =>
request(
"https://swapi-graphql.netlify.app/.netlify/functions/index",
allFilms,
{ first: 10 }
),
});
if (isLoading) {
return <p>Request is loading!</p>;
}
if (isError) {
return <p>Request has failed :(!</p>;
}
return (
<ul>
{data.allFilms.edges.map(({ node: { id, title } }) => (
<li key={id}>
<h2>{title}</h2>
</li>
))}
</ul>
);
};
When our request is in-flight, the component will render a loading message.
If the request was to ever error, the component will render an error message.
Finally, if the request is not in-flight, no errors exist, and data is available from our request, we’ll have the component render the final intended output—a list of Star Wars films fetched from the API.
Test the above in this Codesandbox link.
React Query provides a useMutation() function to allow mutations to be conducted from React components. Unlike queries, mutations are used to create, update or delete data on the server or otherwise perform server side effects.
Like the useQuery()
function, the useMutation()
function receives an asynchronous function that returns a promise. The publicly accessible Star Wars GraphQL API we’re using doesn’t have root mutation fields for us to use but we’ll assume a mutation, called addFilm
, exists that allows us to add a new film to the list of films saved in our database.
import { gql, request } from "graphql-request";
import { useMutation } from "@tanstack/react-query";
const addFilm = gql`
mutation addFilm($title: String!, $releaseDate: String!) {
addFilm(title: $title, releaseDate: $releaseDate) {
id
title
releaseDate
}
}
`;
The addFilm
mutation will accept title
and releaseDate
as variables and when successful will return the id
of the newly created film, along with the title
and releaseDate
that were passed in.
In the component function, we’ll leverage the useMutation()
hook to help trigger the mutation when a button is clicked. We’ll call useMutation()
and supply the asynchronous function that triggers the GraphQL mutation request.
import { gql, request } from "graphql-request";
import { useMutation } from "@tanstack/react-query";
const addFilm = gql`
...
`;
const SomeComponent = () => {
const mutation = useMutation({
mutationFn: async (newFilm) =>
request(
"https://swapi-graphql.netlify.app/.netlify/functions/index",
addFilm,
newFilm
),
});
};
The useMutation()
hook returns a mutation
object that contains details about the mutation request (isLoading
, isError
, etc.). It also contains a mutate()
function that can be used anywhere in our component to trigger the mutation.
We’ll have a button trigger the mutate()
function when clicked. Additionally, we can display some messaging to the user whenever the mutation request is either in flight or has errored.
import { gql, request } from "graphql-request";
import { useMutation } from "@tanstack/react-query";
const addFilm = gql`
mutation addFilm($title: String!, $releaseDate: String!) {
addFilm(title: $title, releaseDate: $releaseDate) {
id
title
releaseDate
}
}
`;
const SomeComponent = () => {
const { mutate, isLoading, isError } = useMutation({
mutationFn: async (newFilm) =>
request(
"https://swapi-graphql.netlify.app/.netlify/functions/index",
addFilm,
newFilm
),
});
const onAddFilmClick = () => {
mutate({
title: "A New Hope",
releaseDate: "1977-05-25",
});
};
return (
<div>
<button onClick={onAddFilmClick}>Add Film</button>
{isLoading && <p>Adding film...</p>}
{isError && <p>Uh oh, something went wrong. Try again shortly!</p>}
</div>
);
};
This essentially summarizes the fundamentals of initiating projects with React and GraphQL. By using a GraphQL client library, we can leverage hooks and utilities to conduct GraphQL queries and mutations efficiently. The information fetched or manipulated using these queries and mutations allows us to display varied UI components and elements which ensures the application is dynamically responsive to user interactions and data alterations.
In this article, we explored the basics of integrating GraphQL with a React application, utilizing React Query as our client library. We wrapped our application in a QueryClientProvider
to make use of React Query functionalities in our application component tree and proceeded to make GraphQL queries and mutations using the useQuery()
and useMutation()
hooks, respectively.
Understanding the principles of GraphQL and how it integrates with React is important since it can sometimes offer a more efficient alternative to REST when dealing with APIs. By leveraging libraries like React Query, we can also significantly simplify the process of fetching, synchronizing, and managing the state of GraphQL server-side data in their React applications.
We’ll continue discussing GraphQL and React with some follow-up articles soon. Stay tuned!
]]>Virtually all modern frontend frameworks embrace the idea that a web application should be split into smaller reusable pieces called components, where each component is just a piece of UI, some logic and data required to render that piece. Furthermore, components may communicate with other components, giving birth to the idea that any web application is just a tree of components.
This guide provides a comparative analysis of the Angular framework and React library, focusing on their approaches to components. It’ll cover component creation, lifecycle, communication, and how these technologies allow components to detect and react to changes as users interact with the UI.
Angular is a TypeScript-first, full-fledged framework created by Google that allows you to create client or server-rendered enterprise-grade web applications at any scale. Some core concepts of this framework are components, services and directives, all of which are just classes annotated with @Compnent()
, @Injectable()
and @Directive()
decorators, respectively.
Components encapsulate the following:
Components may use services and directives. Services allow keeping related functionality in a separate class, and directives allow components to add or modify the behavior of their views.
Angular components rely heavily on dependency injection, enabling them to use the services they need to work. The Angular Injector provides services to components from their constructor or using the inject()
function.
Let’s create an Angular project. Start by installing the Angular CLI, a convenient tool for authoring Angular apps.
npm install -g @angular/cli
Next, create a project called my-angular-app
ng new my-angular-app
Follow the prompts and select all the default options from the CLI to create the angular project as shown below:
Our app uses Angular 17, the latest version of Angular at the time of writing.
To preview the running application, run npm start
.
React is a JavaScript library that creates reusable client and server-side components for web applications. It was created by the Meta team in 2013.
In React, components are plain JavaScript functions. The React team has the philosophy that JavaScript is in charge of the markup rendered on the screen, which is why functions that are React components hold the logic, the styles and the markup at once.
Although not compulsory and not coupled to the React library, the templating language commonly used for the markup is JSX, an HTML-like representation of UI elements and components.
Let’s create a basic client-side React app. Run the following command in your terminal to create a typescript-powered react project in a folder called my-react-app
.
npm create vite@latest my-react-app -- --template react-ts
Next, run the following commands to install all the project dependencies and preview the application in your browser.
cd my-react-app
npm install
npm run dev
To talk about components in both technologies, we will incrementally build a simple counter application like the one below.
Our counter app above will consist of three components: the app shell, the counter and a counter button. A representation of our app as a tree of components is shown below.
Our application logic is simple. The app renders some dummy text that includes the current time, a random image of a clock and the counter component, which allows the user to increment a number by interacting with the counter button. When the counter’s value is greater than 6, the button is disabled.
This simple app allows us to do some interesting things. To take a closer look at the component architecture of Angular and React, our focus will be on the following key areas:
Now, we will build the counter and counter button components and then discuss the following.
Let us create our root App component and render some basic markup.
Update the src/App.tsx
file with the following:
import "./app.css";
function App() {
return (
<div>
<h1>Hello World time is 2:46:48 </h1>
<img src="" alt="clock" />
</div>
);
}
React components are plain JavaScript functions that must begin with a capital letter; notice that the returned JSX is wrapped in a single parent element (a div in our case). React components must return a single root element. This idea is not React-specific—all functions in Javascript can only return a single thing.
For TypeScript to handle JSX templates correctly, the file name has to have a
.tsx
extension.
We also imported the styles for our component by importing an app.css
file.
In the src/app
folder, let’s create a component called app.component.ts
by running these commands:
cd src/app
touch app.component.ts
Update this file with the following:
@Component({
selector: 'app-root',
template: `
<h1>Hello World time is 2:46:48</h1>
<img src="https://www.telerik.com" alt="clock" srcset="" />
`,
styles: [
`
* {
display: block;
}
`,
],
standalone: true,
})
export class AppComponent implements {
}
As shown above, Angular components are classes decorated with the @Component()
decorator, which holds the options used to configure the component.
The template and style hold the component’s markup and style, respectively. Templates are HTML markup that may use the Angular templating language.
Templates may also be passed using a templateURL
prop holding a relative path to an HTML file. Also, styles can be passed using a styleURLs
prop, an array of relative paths pointing to CSS files for the component. For a given component, the style and styleURLs
options can be used at once, but for templates, you can only choose one template or templateURL
. When both are specified, the latter takes precedence.
The selector
property is a unique name for the component when it is rendered in another component, and ours is called app-root
.
Older versions of Angular required that, before components were used, they had to be registered in a Module (a class decorated with @NgModule()
) to configure it further. This is no longer necessary; the standalone prop makes our component self-contained to manage and configure all its dependencies. This is the recommended way to create components.
Let’s make our App.tsx
React component and the app.component.ts
Angular component visible in the browser. The key takeaway is that each technology renders components in an HTML file. This file is sent to the browser with the bundled JavaScript, CSS and other things the client requires and served by their respective build tools.
Update the main.tsx
file in your React project to match the following:
import ReactDOM from "react-dom/client";
import App from "./App.tsx";
ReactDOM.createRoot(document.getElementById("root")!).render(<App />);
The ReactDOM library enables rendering our React components to the browser using its createRoot
method. This method is called and fed an HTML DOM element. It receives an element with an id of root
in the index.html
file in our project root folder.
The render()
function then receives the <App/>
component and renders it to the browser.
Update the main.ts
file in a root folder to match the following:
import { bootstrapApplication } from "@angular/platform-browser";
import { appConfig } from "./app/app.config";
import { AppComponent } from "./app/app.component";
bootstrapApplication(AppComponent).catch((err) => console.error(err));
Similarly, the @angular/platform-browser
module allows us to mount our angular components in the browser using its bootstrapApplication()
function, which accepts our AppComponent
class. This function mounts the AppComponent
to the main/index.html
file using its app-root
selector.
If we run our application on both platforms, we should see our app display a static time and an empty image, as shown below.
Yes, this is expected because our template holds hardcoded values, and the image has an empty src
property, as shown below.
<h1>Hello World time is 2:46:48 </h1>
<img src='' alt='clock' />
Remember, apart from the rendered UI template, components also hold data. Data binding allows components to include their data in the rendered components’s UI template. Let’s now bind some data to our components.
Update the App.tsx
file to look like so:
function App() {
const imageURL =
"https://images.unsplash.com/photo-1456574808786-d2ba7a6aa654?w=800&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8NHx8Y291bnR8ZW58MHx8MHx8fDA%3D";
return (
<div>
<h1>Hello World time is {new Date().toLocaleTimeString()}</h1>
<img src={imageURL} alt="" />
</div>
);
}
Data binding in React is done between a {
and }
, which could contain any primitive type, such as a string like the JavaScript expression imageURL
above. An expression is just something that produces a value, like our new Date().toLocaleTimeString()
function call that returns a value. The curly-brace syntax {
and }
is the only method for binding data in React components.
Update the app.component.ts
file with the following:
@Component({
selector: "app-root",
template: `
<h1>Hello World time is {{ now() }}</h1>
<img [src]="" alt="clock" [srcset]="" />
`,
styles: [
/*...*/
],
standalone: true,
})
export class AppComponent {
now() {
return new Date().toLocaleTimeString();
}
imageURL =
"https://images.unsplash.com/photo-1456574808786-d2ba7a6aa654?w=800&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8NHx8Y291bnR8ZW58MHx8MHx8fDA%3D";
}
Angular provides us with four different syntaxes for data binding, which are:
{{ and }}
: This is the default interpolation syntax for embedding primitives or expressions that return them. We used it to embed the time string by invoking the now()
method we included in our AppComponent.
Note that doing something like {{new Date().toLocaleTimeString()}}
in Angular component templates is not supported. The framework allows developers to modify the {{and}}
syntax by setting an interpolation property in the options fed to @Component
. For example passing interpolation: ["|_","_|"]
will enable using |_ and _|
in our templates.[ and ]
: This is used for property binding, e.g., in the [src]
property above that references our AppComponent
, the imageURL
instance variable.( and )
: This is used for binding events, usually when we want to bind functions to components.[( and )]
: This is used for two-way data binding in Angular components, commonly used with forms.Running our app on both platforms, we get the current time printed on the screen.
At this juncture, before we explain the remaining component-related concepts of Angular and React, let’s build the counter and counter button components.
We need to create two files in our src
folder. These are Counter.tsx
and CounterButton.tsx
.
Update the Counter.tsx
file with the following:
import { CSSProperties } from "react";
export default function Counter() {
const styles: CSSProperties = {
border: "2px solid red",
};
return (
<div style={styles}>
<h2>counter value is 0</h2>
</div>
);
}
Next, update the CounterButton.tsx
file with the following:
function CounterButton() {
return (
<button onClick={() => onClick(1)} disabled={disabled}>
increment
</button>
);
}
We need to create two files in our src/App
folder. These are counter.component.ts
and counter-button.component.ts
.
Update the counter.component.ts
file to match the following:
import { Component, EventEmitter, Input, OnInit, Output } from "@angular/core";
import { CounterButton } from "./counter-button.component";
@Component({
selector: "counter-comp",
template: `
<div style="border:2px solid red">
<h2>counter value is 0</h2>
</div>
`,
styles: [],
standalone: true,
})
export class CounterComponent {
constructor() {}
}
Next, update the counter-button.component.ts
file with the following:
import { Component } from "@angular/core";
@Component({
selector: "counter-button",
template: ` <button>increment</button> `,
styles: [],
standalone: true,
})
export class CounterButton {
constructor() {}
}
We want to show how components render other components. We want to have all our components connected to end up with this structure.
Let’s do that in both applications. The idea is simple: We will connect the counter button component to the counter component and then connect the counter to the app component.
In React, Component A can be composed of Component B if A imports B and embeds it in its returned template.
Let’s now include the CounterButton
in the Counter
.
import CounterButton from "./CounterButton";
export default function Counter() {
const styles: CSSProperties = {
border: "2px solid red",
};
return (
<div style={styles}>
<h2>counter value is 0</h2>
<h1>This is a counter app</h1>
<CounterButton />
</div>
);
}
Next, let’s connect the Counter
component to the AppComponent
component.
import Counter from "./Counter";
function App() {
const imageURL =
"https://images.unsplash.com/photo-1456574808786-d2ba7a6aa654?w=800&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8NHx8Y291bnR8ZW58MHx8MHx8fDA%3D";
return (
<div>
<h1>Hello World time is {new Date().toLocaleTimeString()}</h1>
<img src={imageURL} alt="" />
<h1>This is a counter app</h1>
<Counter />
</div>
);
}
In Angular, Component A can be composed of Component B if A imports B and includes B in its imports array in its @Component()
decorator options.
Let’s now include the CounterButton
in Counter
:
import { CounterButton } from "./counter-button.component";
@Component({
selector: "counter-comp",
template: `
<div style="border:2px solid red">
<h2>counter value is 1</h2>
<counter-button />
</div>
`,
styles: [],
standalone: true,
imports: [CounterButton],
})
export class CounterComponent {
constructor() {}
}
Likewise, let’s connect the Counter
to the AppComponent
:
import { Component, OnInit } from "@angular/core";
import { CounterComponent } from "./counter.component";
@Component({
selector: "app-root",
template: `
<h1>Hello World time is {{ now() }}</h1>
<img [src]="imageURL" alt="" [srcset]="" />
<counter-comp />
`,
styles: [
`
* {
display: block;
}
`,
],
standalone: true,
imports: [CounterComponent],
})
export class AppComponent {
constructor() {}
now() {
return new Date().toLocaleTimeString();
}
imageURL =
"https://images.unsplash.com/photo-1456574808786-d2ba7a6aa654?w=800&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8NHx8Y291bnR8ZW58MHx8MHx8fDA%3D";
}
If we run both applications now, we should see the counter displayed on the browser, as shown below.
So far, our application displays static data, so let’s add some states to our app. Hold on, what is the state? State is the data held by a component that is typically subject to change over time as the user interacts with the component’s template’s UI.
Our Counter component currently displays a static value. Let’s now add some state to hold the counter value and a function to increment the counter.
React provides several functions (called hooks) for state management, such as useState()
and useReducer()
. The React ecosystem also consists of libraries such as Redux, Mobx, Preact, etc., which are used to simplify state management.
Here, we will only describe state management using useState()
. The fundamental idea about component state is similar irrespective of the option chosen.
Let’s now add state to our counter component by updating the Counter.tsx
file with the following:
import { useState } from "react";
export function Counter() {
const [count, setCount] = useState(0);
function updateCounter(val: number) {
setCount(count + val);
}
const styles: CSSProperties = {
border: "2px solid red",
};
return (
<div style={styles}>
<h2>counter value is {count}</h2>
<CounterButton />
{props.children}
</div>
);
}
Our counter value, by default, holds a value of 1. The useState()
call returns an array with two properties—the value (count
) and a setter function (setCount
).
We also define a function called updateCounter()
that accepts a number and then updates the counter’s value using the setter function. Later, when we discuss change detection in components, the explanation will be clearer on why React specifies using a setter instead of directly changing the value using something like count+= val
.
In Angular, state can be maintained using regular instance variables of your component class that can be set to primitive types, observables (using RxJs) and signals. Also, the Angular ecosystem provides developers with tools like the NgRx store (based on Redux) to further simplify the task of managing the app state at scale.
We will describe how to use the component’s instance variable for state management.
Let’s update our Counter component by updating the counter-component.ts
file.
@Component({
selector: "counter-comp",
template: `
<div style="border:2px solid red">
<h2>counter value is {{ count }}</h2>
<counter-button />
</div>
`,
styles: [],
standalone: true,
imports: [CounterButton],
})
export class CounterComponent {
constructor() {}
count = 1;
updateCounter(val: number) {
this.count += val;
}
}
We included an instance variable called count
and a function called updateCounter
that updates it.
Component communication focuses on how components interact; the interaction is typically done by passing data between components.
In React and Angular, data flow from parent to child is unidirectional, i.e., parents can only pass data to children rather than the other way around.
All React components receive data via props. A prop is just a key-value pair where the key name is a string, and the value can be a primitive or an object.
Our counter button is our concern at this point. It needs to be able to update the counter, and it also needs to be disabled when the counter value is greater than 5. We want our counter component to pass this data to this component.
Let’s configure our CounterButton component to receive some props.
interface CounterButtonProps {
handleIncrement: (val: number) => void;
disabled: boolean;
}
function CounterButton({ handleIncrement, disabled }: CounterButtonProps) {
return (
<button onClick={() => handleIncrement(1)} disabled={disabled}>
increment
</button>
);
}
The CounterButton component accepts two props: a disabled
prop to disable the button and the handleIncrement
prop, which is a function to increment the counter. Let’s pass these props from our Counter
component.
export function Counter(props: PropsWithChildren) {
const [count, setCount] = useState(0);
function updateCounter(val: number) {
setCount(count + val);
}
const styles: CSSProperties = {
border: "2px solid red",
};
return (
<div style={styles}>
<h2>counter value is {count}</h2>
{count > 5 ? <h4>count is > 5</h4> : null}
<CounterButton
handleIncrement={(val: number) => updateCounter(val)}
disabled={count > 5}
/>
{props.children}
</div>
);
}
The Counter
component feeds data with the two properties with the respective values supported by the CounterButton
.
React components can also be passed as props in React. So suppose our Counter component needs a prop named “special” which is expected to be a component. You can do something like
<CounterButton special={<SomeComponent/>}/>
.
Similarly, Angular data is passed between parent and child components as key-value pairs; however, unlike React, Angular components cannot be passed as values between parent and child components.
Let’s go the other way around. Let’s feed data from the CounterComponent
to the CounterButton
component this time.
@Component({
selector: "counter-comp",
template: `
<div style="border:2px solid red">
<h2>counter value is {{ count }}</h2>
<counter-button
(handleIncrement)="updateCounter($event)"
[disabled]="count > 5"
/>
</div>
`,
styles: [],
standalone: true,
imports: [CounterButton],
})
export class CounterComponent {
constructor() {}
count = 1;
updateCounter(val: number) {
this.count += val;
}
}
Let’s update the counter button in the counter-button.component.ts
file to receive the props.
@Component({
selector: 'counter-button',
template: `
<button (click)="incrementCounter()" [disabled]="disabled">
increment
</button>
`,
styles: [],
standalone: true,
})
export class CounterButton {
constructor() { }
@Input("disabled") disabled!: boolean;
@Output('handleIncrement')
handleIncrementEmitter = new EventEmitter();
incrementCounter() {
this.handleIncrementEmitter.emit(1);
}
}
When we start both applications, we see we can increment the counter, and the counter button gets disabled when the value exceeds 5.
When we look at both applications, we notice that we can increment both counters. Let’s look closely at both applications
In the React application, we notice that only the counter gets incremented, but the time remains the same.
But in the Angular application, each time we increment the counter, the time is also updated in the app UI, as shown below.
This leads us to the question: How do Angular and React detect that a component has changed, and how does the changed component render itself and update the UI?
In React, state change is what enables React to know when a component needs to rerender. If you look at our Counter component, we use the useState
hook to manage the value of the count whenever we call our setCount
function. React knows that we most likely have changed, and it needs to re-render our Counter component and update the app UI with the new value, but how does this work?
Remember our app component tree as shown below.
Let’s take a step back. Before our app tree (i.e., App, Counter and CounterButton) is rendered on the browser window, React represents our entire app as a tree of plain JavaScript objects (all the JSX we have included in our React app, and our App component functions are the nodes in this tree). This tree is referred to as the virtual DOM. This virtual DOM is then parsed and rendered on the browser’s DOM.
The browser’s DOM maintains references to all the functions and data that the virtual DOM knows.
As the user interacts with the app and triggers any state-changing function—e.g., when the user clicks the counter button and calls its handleIncrement()
function, which in turn calls the setCount()
function to update the counter—React is aware that something has changed in our counter.
React then creates a new version of the virtual DOM from the old one (immutability) with the count updated and then compares the old and the new virtual DOM to see what has changed using its special diffing algorithm.
React then only updates the counter and its subtree, i.e., the Counter
and CounterButton
, but not the App
component in the browser’s DOM to reflect the new change. This efficient process React uses to update only what is necessary is referred to as reconciliation.
When an Angular component is created, the framework automatically creates a change detector for it. There are two types of change detection strategies supported by Angular, and these are:
By default, all components use the default strategy, but you can pass a changeDetector
property to change this behavior.
In Angular applications, change detection is typically triggered when a DOM event is handled such as a button click, or when using the special async pipe, which may modify some internal state of the component and change the template the component renders.
Generally, when change detection occurs in Angular, these things happen:
Let’s look at our application to see what happens in this framework’s default change detection mode.
When the counter button is clicked and we call its incrementCounter
function since this function is bound to a DOM event, a click in our case, the Zone.js library wraps the incrementCounter
function. Once this function is done executing, it marks the counter button component and its ancestors as dirty, then notifies Angular that it needs to look at the App component tree.
Angular traverses this tree from top to button, but it doesn’t look at its children since it already sees that the App component is dirty. It re-renders the whole tree, and the components update to reflect the new value. This is why the time changes in the App component after incrementing the counter.
To understand the OnPush
strategy of change detection, let’s modify our app tree temporarily and include a component called OnPushComponent
. We won’t create a new file for this dummy component since it is for demonstration.
@Component({
selector: "on-push-comp",
template: ` <h1>the time is{{ now() }}</h1> `,
styles: [],
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class OnPushComponent {
constructor() {}
now() {
return new Date().toLocaleTimeString();
}
}
Notice that this component uses the OnPush change detection strategy. Let’s register it in our app component.
@Component({
selector: 'app-root',
// templateUrl: './x.htm',
template: `
<h1>Hello World time is {{ now() }}</h1>
<img [src]="imageURL" alt="" />
<h1>This is a counter app</h1>
<counter-comp>
<!-- <span>this is rendered in counter</span> -->
</counter-comp>
<on-push-comp />
`,
styles: [
`
* {
display: block;
}
`,
],
standalone: true,
imports: [CounterComponent, OnPushComponent],
providers: [WayooService],
changeDetection: ChangeDetectionStrategy.OnPush,
})
A component marked with the OnPush
change event is not marked as dirty if its data or children subtree does not change. It is only marked as dirty in the following scenarios:
changeRef
object retrieved via dependency injection.So, in our mini app, if the user increments the counter, Zone.js notifies Angular to check for dirty components and rerender the application. When Angular reaches our stubborn OnPush
component, it tells it that it hasn’t changed and does not need a re-render, so it is not re-rendered, as shown below.
It is important to carefully analyze the performance benefits and clearly understand the behavior of the OnPush
change detection strategy before using it so that you don’t end up with a broken application.
We want to enable our Counter component to render a piece of UI when the counter value exceeds 5. Conditional rendering allows components to render something when certain conditions are met.
Update the Counter.tsx
file to match the following:
export function Counter(props: PropsWithChildren) {
return (
<div style={styles}>
<h2>counter value is {count}</h2>
{count > 5 ? <h4>count is > 5</h4> : null}
<CounterButton
onClick={(val: number) => updateCounter(val)}
disabled={count > 5}
>
increment
</CounterButton>
{props.children}
</div>
);
}
Components can conditionally render other components using the ternary operator. For more control, you can also pass a function that returns a value using normal if-else statements.
Update the counter-component.ts
file to look like so:
@Component({
selector: 'counter-comp',
template: `
<div style="border:2px solid red">
<h2>counter value is {{ count }}</h2>
@if (count > 5) {
<h4>count is > 5</h4>
}
<counter-button
(handleIncrement)="updateCounter($event)"
[disabled]="count > 5"
>
increment
</counter-button>
<ng-content></ng-content>
</div>
`,
styles: [],
standalone: true,
imports: [CounterButton],
})
We can conditionally render components using the @if
syntax.
React provides us with custom hooks. Let’s update the Counter.tsx
file to include a custom hook called useCounter
.
function useCounter() {
const [count, setCount] = useState(0);
function updateCounter(val: number) {
setCount(count + val);
}
return { updateCounter, count };
}
export default useCounter;
You can put the useCounter
hook in a separate file, but we are fine with our setup in this case.
Not all React components return UI. Here, we created a hook, which is just a function that, in our case, uses the useState
hook to maintain the value of the counter and a function to update it.
Let’s include this hook in our counter component, as shown below.
export function Counter(props: PropsWithChildren) {
const { updateCounter, count } = useCounter();
const styles: CSSProperties = {
border: "2px solid red",
};
return (
<div style={styles}>
<h2>counter value is {count}</h2>
{count > 5 ? <h4>count is > 5</h4> : null}
<CounterButton
onClick={(val: number) => updateCounter(val)}
disabled={count > 5}
>
increment
</CounterButton>
{props.children}
</div>
);
}
Angular allows components to share logic via services. Let’s create a service called CounterService
in a file called counter.service.ts
import { Injectable } from "@angular/core";
@Injectable({
providedIn: "root",
})
export class CounterService {
count = 1;
x = 1;
updateCounter(val: number) {
this.count += val;
}
}
Services are classes decorated with the @Injectable
decorator. To use this service, we need to register it. Depending on our application needs, we can register a service in a component high in our component tree. This will make the component accessible to other child components.
In our case, since it’s only our counter that needs this service, let’s register it there. Update the counter-component.ts
file as shown below:
import {
Component,
EventEmitter,
Input,
OnInit,
Output,
inject,
} from "@angular/core";
import { CounterButton } from "./counter-button.component";
import { CounterService } from "./counter.service";
@Component({
selector: "counter-comp",
template: `
<div style="border:2px solid red">
<h2>counter value is {{ counterService.count }}</h2>
@if (counterService.count > 5) {
<h4>count is > 5</h4>
}
<counter-button
(handleIncrement)="counterService.updateCounter($event)"
[disabled]="counterService.count > 5"
>
increment
</counter-button>
<ng-content></ng-content>
</div>
`,
styles: [],
standalone: true,
imports: [CounterButton],
providers: [CounterService],
})
export class CounterComponent {
counterService: CounterService = inject(CounterService);
}
The counter service is registered to provide our counter service via its providers
array; it is then accessed in the counter
class using dependency injection using the inject
function.
Components also have a lifecycle that allows developers to perform actions on them when the component is created, updated or destroyed. It allows the user to write some logic at each point.
React provides developers with several hooks to write some code in the component lifetime, such as the following:
useLayoutEffect()
: This allows you to write some code before a component is rendered to the screen.useEffect()
: This hook allows you to write some code when the component mounts, properties change and the component gets deleted. Typically, this is where data gets fetched from the server for a component and where you write some clean-up code when the component gets destroyed. Let’s write some code to print a message to the screen each time our counter value changes.import { CSSProperties, PropsWithChildren, useEffect, useState } from "react";
export function Counter(props: PropsWithChildren) {
const { updateCounter, count } = useCounter();
useEffect(() => {
console.log("count changed");
return () => console.log(" this function runs the cleanup");
}, [count]);
const styles: CSSProperties = {
border: "2px solid red",
};
return (
<div style={styles}>
<h2>counter value is {count}</h2>
{count > 5 ? <h4>count is > 5</h4> : null}
<CounterButton
onClick={(val: number) => updateCounter(val)}
disabled={count > 5}
>
increment
</CounterButton>
{props.children}
</div>
);
}
At the simplest level, to write some code when a component gets created, we can include some code in the component’s constructor. However, Angular also provides developers with a long list of methods to add to their components when the component is initialized (data-fetching operations are typically done here), when its view and all its children’s views are rendered, when inputs change, etc. You can learn more about this here.
Talking about modern frontend frameworks without talking about components is almost impossible. This guide allows developers to see hands-on component architecture in Angular and React; hopefully, this knowledge can help new and seasoned developers see the need to explore other frameworks and use them in their future work despite the nuances, similarities and differences between them.
Continue exploring Angular or React topics in our Basics series.]]>
In the evolving world of web applications, real-time functionality has become a pivotal feature, enabling interactive and dynamic user experiences.
Whether it’s live chats, notifications or collaboration tools, having instant feedback is critical for user experience. Great examples are chat applications where users can see each others’ messages instantly or editor tools, such as Figma or Google Docs, that allow many users to collaborate together in real-time.
All of this is made possible by real-time technologies, such as WebSockets. In this article, we will take advantage of WebSockets and build a real-time application using React on the client side and Fastify with Node.js on the server-side.
WebSocket is a powerful communication protocol that enables two-way, full-duplex communication between a client and a server over a single, long-lived connection. Unlike traditional HTTP requests, which are stateless and involve opening a new connection for each request, WebSockets maintain a persistent connection.
You can find the full code example for this tutorial in the GitHub repository.
Let’s start by creating a client-side React app with Vite and server-side project with Fastify.
npm create vite@latest client -- --template react
cd client
npm install
npm run dev
A newly created Vite app runs on port 5173, so visit http://localhost:5173
in your browser to access it.
After the Vite project is created, we need to create the server side.
mkdir server
cd server
npm init -y
npm install fastify
server/index.mjs
import fastify from "fastify";
const app = fastify({
logger: true,
});
app.get("/", async (request, reply) => {
return { hello: "world" };
});
try {
await app.listen({ port: 3000 });
app.log.info(`Server is running on port ${3000}`);
} catch (error) {
app.log.error(error);
process.exit(1);
}
"dev"
script to run the server.server/package.json
{
"name": "server",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"dev": "node --watch index.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"fastify": "^4.24.3"
}
}
Note that the node --watch
command is only available since Node 18. If you’re using an older version, you can use Nodemon instead.
After running the npm run dev
command, the Fastify server should start on port 3000
. After visiting http://localhost:3000
, you should see the following response in the browser.
There are multiple ways of implementing WebSockets on the server and client side. For example, we could use libraries, such as ws and socket.io. However, Fastify has a core library called @fastify/websocket that provides WebSocket functionality and integrates well with the Fastify framework. Therefore, if you’re using Fastify in your project, consider using the @fastify/websocket
library. Otherwise, you can use other solutions.
Let’s install @fastify/websocket
and @fastify/cors
in the server
directory.
npm install @fastify/websocket @fastify/cors
If your project uses TypeScript, make sure to also install types.
npm i @types/ws -D
Next, we need to register the @fastify/websocket
plugin to start listening for messages and the @fastify/cors
plugin to allow connections from other ports. We need to do this because the React app runs on http://localhost:5137
, while the Fastify app will run on http://localhost:3000
.
server/index.mjs
import Fastify from "fastify";
import fastifyWebSockets from "@fastify/websocket";
import cors from "@fastify/cors";
const fastify = Fastify({
logger: true,
});
/**
* Register cors to allow all connections. Note that in production environments, you should
* narrow down domains that should be able to access your server.
*/
fastify.register(cors);
/**
* Register the Fastify WebSockets plugin.
*/
fastify.register(fastifyWebSockets);
/**
* Register a new handler to listen for WebSocket messages.
*/
fastify.register(async function (fastify) {
fastify.get(
"/online-status",
{
websocket: true,
},
(connection, req) => {
connection.socket.on("message", msg => {
connection.socket.send(`Hello from Fastify. Your message is ${msg}`);
});
}
);
});
fastify.get("/", async (request, reply) => {
return { hello: "world" };
});
try {
await fastify.listen({ port: 3000 });
fastify.log.info(`Server is running on port ${3000}`);
} catch (error) {
fastify.log.error(error);
process.exit(1);
}
Fastify will forward all WebSocket connections to the /online-status
endpoint. When a new message is received, a response is sent immediately.
connection.socket.send(`Hello from Fastify. Your message is ${msg}`);
Next, let’s modify our React app to send and receive messages from the server.
client/src/App.jsx
import { useEffect } from "react";
import "./App.css";
/**
* Establish a new WebSocket connection.
*/
const ws = new WebSocket(`ws://localhost:3000/online-status`);
/**
* When a WebSocket connection is open, inform the server that a new user is online.
*/
ws.onopen = function () {
ws.send("hello from react");
};
function App() {
useEffect(() => {
/**
* Listen to messages and change the users' online count.
*/
ws.onmessage = message => {
console.log("message from server:", message.data);
};
}, []);
return <div></div>;
}
export default App;
We establish a new WebSocket connection and send the “hello from react” message when the connection is opened.
Now we have a working WebSocket connection. Let’s modify the client-side further to display the count of all online users sent from the server. Moreover, we can add a select to allow users to change their online status.
client/src/App.jsx
import { useEffect, useState } from "react";
import "./App.css";
/**
* Get a random user ID. This is fine for this example, but for production, use libraries like paralleldrive/cuid2 or uuid to generate unique IDs.
*/
const userId = localStorage.getItem("userId") || Math.random();
localStorage.setItem("userId", userId);
/**
* Establish a new WebSocket connection.
*/
const ws = new WebSocket(`ws://localhost:3000/online-status`);
/**
* When a WebSocket connection is open, inform the server that a new user is online.
*/
ws.onopen = function () {
ws.send(
JSON.stringify({
onlineStatus: true,
userId,
})
);
};
function App() {
/**
* Store the count of all users online.
*/
const [usersOnlineCount, setUsersOnlineCount] = useState(0);
/**
* Store the selected online status value.
*/
const [onlineStatus, setOnlineStatus] = useState();
useEffect(() => {
/**
* Listen to messages and change the users online count.
*/
ws.onmessage = message => {
const data = JSON.parse(message.data);
setUsersOnlineCount(data.onlineUsersCount);
};
}, []);
const onOnlineStatusChange = e => {
setOnlineStatus(e.target.value);
if (!e.target.value) {
return;
}
const isOnline = e.target.value === "online";
ws.send(
JSON.stringify({
onlineStatus: isOnline,
userId,
})
);
};
return (
<div>
<div>Users Online Count - {usersOnlineCount}</div>
<div>My Status</div>
<select value={onlineStatus} onChange={onOnlineStatusChange}>
<option value="">Select Online Status</option>
<option value="online">Online</option>
<option value="offline">Offline</option>
</select>
</div>
);
}
export default App;
Let’s digest the code step by step. At first, we create a random ID for the user and save it in the local storage so it’s not recreated on every page reload.
const userId = localStorage.getItem("userId") || Math.random();
localStorage.setItem("userId", userId);
Further, when a WebSocket connection is opened, the server is notified that a new user has visited the page.
/**
* When a WebSocket connection is open, inform the server that a new user is online.
*/
ws.onopen = function () {
ws.send(
JSON.stringify({
onlineStatus: true,
userId,
})
);
};
After receiving this message, the server will broadcast a message to all subscribed clients that the online users status has changed. We will implement it in a moment.
We have two states. The first one, usersOnlineCount
, will store the count of all online users. This information will be sent from the server. The second state stores the information about the user’s selected online status.
/**
* Store the count of all users online.
*/
const [usersOnlineCount, setUsersOnlineCount] = useState(0);
/**
* Store the selected online status value.
*/
const [onlineStatus, setOnlineStatus] = useState();
With useEffect
, we listen for new messages and update the users online state accordingly.
useEffect(() => {
/**
* Listen to messages and change the users online count.
*/
ws.onmessage = message => {
const data = JSON.parse(message.data);
setUsersOnlineCount(data.onlineUsersCount);
};
}, []);
Finally, the onOnlineStatusChange
status method keeps the state in sync with the select
element and notifies the server when the user’s status is changed.
const onOnlineStatusChange = e => {
setOnlineStatus(e.target.value);
if (!e.target.value) {
return;
}
const isOnline = e.target.value === "online";
ws.send(
JSON.stringify({
onlineStatus: isOnline,
userId,
})
);
};
Let’s update the server so it stores online users and updates the count whenever the online users status is changed.
server/index.mjs
import Fastify from "fastify";
import fastifyWebSockets from "@fastify/websocket";
import cors from "@fastify/cors";
const fastify = Fastify({
logger: true,
});
/**
* Register cors to allow all connections. Note that in production environments, you should
* narrow down domains that should be able to access your server.
*/
fastify.register(cors);
/**
* Register the Fastify WebSockets plugin.
*/
fastify.register(fastifyWebSockets);
const usersOnline = new Set();
/**
* Register a new handler to listen for WebSocket messages.
*/
fastify.register(async function (fastify) {
fastify.get(
"/online-status",
{
websocket: true,
},
(connection, req) => {
connection.socket.on("message", msg => {
const data = JSON.parse(msg.toString());
if (
typeof data === "object" &&
"onlineStatus" in data &&
"userId" in data
) {
// If the user is not registered as logged in yet, we add this user's id.
if (data.onlineStatus && !usersOnline.has(data.userId)) {
usersOnline.add(data.userId);
} else if (!data.onlineStatus && usersOnline.has(data.userId)) {
usersOnline.delete(data.userId);
}
/**
* Broadcast the change in online users status to all subscribers.
*/
fastify.websocketServer.clients.forEach(client => {
if (client.readyState === 1) {
client.send(
JSON.stringify({
onlineUsersCount: usersOnline.size,
})
);
}
});
}
});
}
);
});
fastify.get("/", async (request, reply) => {
return { hello: "world" };
});
try {
await fastify.listen({ port: 3000 });
fastify.log.info(`Server is running on port ${3000}`);
} catch (error) {
fastify.log.error(error);
process.exit(1);
}
On line 20, we have the usersOnline
set that stores the count of currently online users. In a real app, this information could be handled using a solution like Redis, but for this example the above implementation will suffice.
After a user is connected, we listen for messages using connection.socket.on("message", msg => {})
. In the on message
handler, we check if the msg
value received from the client is an object with onlineStatus
and userId
properties. If it is, we check if a user’s status is online or offline. Based on the status, we either add or remove the user’s id from the usersOnline
set.
if (data.onlineStatus && !usersOnline.has(data.userId)) {
usersOnline.add(data.userId);
} else if (!data.onlineStatus && usersOnline.has(data.userId)) {
usersOnline.delete(data.userId);
}
Finally, the users online status change is broadcast to all subscribed clients.
fastify.websocketServer.clients.forEach(client => {
if (client.readyState === 1) {
client.send(
JSON.stringify({
onlineUsersCount: usersOnline.size,
})
);
}
});
That’s it. We have just implemented an app with a real-time functionality. Whenever a new user visits the page, all users who are currently online will be notified about the online status change, as shown in the video below.
In this video, the same app is visited using different browsers to simulate different users. Whenever a new page is opened, the users count is updated immediately in other browsers. It also changes when the online status is changed using the user status select functionality.
We can use a tool like Progress Telerik Fiddler Everywhere to check if the WebSockets were set up correctly and what messages are sent between a client and server. Fiddler Everywhere can be used as a local proxy to intercept and spy on http and web socket requests.
The GIF above shows how to capture traffic to the http://localhost:3000/online-status
endpoint. As we change the online status, Fiddler records the messages sent between the clients and the server. For instance, we can see client messages that are sent when the user changes their online status, as well as messages from the server, which comprise the new online user count. Fiddler Everywhere can show various information about the messages, such as their size, content, when they were sent, who was the sender and more.
If you would like to learn more about how to use Fiddler Everywhere to inspect WebSocket connections and more, check out the documentation.
In this article, we have covered how to build a real-time application using WebSockets, React and Fastify. WebSockets are a great tool for implementing real-time communication. This tutorial should give you an understanding of how to add real-time functionality to your own applications.
Keep in mind the example in this tutorial is very simplified, as its purpose is to showcase how to use WebSockets. A real online status tracking functionality should also have some way of detecting if a user was idle for a specific period of time and then change their status to offline automatically.
]]>Internationalization, often abbreviated as i18n (where 18 represents the number of omitted letters between “i” and “n”), is the process of designing and developing an application in a way that allows for easy localization to different languages, regions and cultural contexts. It involves separating the user interface and content from the application’s codebase to facilitate easy translation and adaptation.
In a previous article, we discussed the importance of internationalization in web applications and some important steps one can take to get there.
In today’s article, we’ll go through a more practical tutorial of adding internationalization to a small React application with the help of the react-intl library.
React-intl, part of FormatJS, is a popular library used to internationalize React applications. It provides React components and an API to format dates, numbers and strings, including pluralization and handling translations.
It also supports both singular and plural message translation, string substitution and rich text formatting, among other things.
In a React application, we can start by first installing the react-intl
library:
npm install react-intl
# or
yarn add react-intl
To begin introducing internationalization to our app, we’ll first need to define our language messages. This is usually done in JSON format and each language is defined in a separate file.
We’ll create a locales/
folder in the src/
directory. Inside locales/
, we’ll create three files: en.json
, es.json
and fr.json
to represent the English, Spanish and French languages.
src/locales/en.json
{
"app.greeting": "Hello, User!",
"app.description": "This is a simple react application"
}
src/locales/es.json
{
"app.greeting": "¡Hola, Usuario!",
"app.description": "Esta es una simple aplicación de react"
}
src/locales/fr.json
{
"app.greeting": "Bonjour, Utilisateur !",
"app.description": "C'est une simple application React"
}
These files will act as our dictionaries, mapping message IDs (like app.greeting
and app.description
) to the message in the respective language.
Now, we can set up our application to use react-intl
. In our main app file (App.js
), we’ll import the necessary components from react-intl
as well as the different language message dictionaries we’ve just created.
import { IntlProvider, FormattedMessage } from "react-intl";
import messages_en from "./locales/en.json";
import messages_es from "./locales/es.json";
import messages_fr from "./locales/fr.json";
We’ll then create an object to store all our dictionaries:
const messages = {
en: messages_en,
es: messages_es,
fr: messages_fr,
};
Next, we’ll wrap our app in the <IntlProvider />
component, provided by react-intl
, which will provide i18n context to all the components in our app. The <IntlProvider />
component requires two props: locale
and messages
.
locale
is a string value that represents the language locale we want to use in our application.messages
is an object that contains the actual text messages (strings) that we want to display in our application.To start, we’ll default the locale to English ('en'
) and reference the english message dictionary we’ve created.
export default function App() {
const locale = "en"; // default locale
return (
<IntlProvider locale={locale} messages={messages[locale]}>
{/* rest of our app goes here */}
</IntlProvider>
);
}
With react-intl
now configured, we can use it in our app. We can use the FormattedMessage component from react-intl
to display text.
export default function App() {
const locale = "en";
return (
<IntlProvider locale={locale} messages={messages[locale]}>
<FormattedMessage id="app.greeting" />
<br />
<br />
<FormattedMessage id="app.description" />
</IntlProvider>
);
}
When saving our changes, we’ll be presented with “Hello, User! This is a simple react application” in the browser, as we’re currently using the English locale.
If we were to change the value of the locale
variable to a different language like French
:
export default function App() {
const locale = "fr";
// ...
}
We would be presented with the same “Hello, User! …” message but in French.
We’ll want to provide the end user the capability to switch between different languages. To achieve this, we can consider storing the locale
in our <App />
component’s state.
import { useState } from "react";
// ...
export default function App() {
const [locale, setLocale] = useState("en");
// ...
}
We can then add buttons to our app that lets the user switch between the English, Spanish and French locales.
import { useState } from "react";
// ...
export default function App() {
const [locale, setLocale] = useState("en");
return (
<IntlProvider locale={locale} messages={messages[locale]}>
<FormattedMessage id="app.greeting" />
<FormattedMessage id="app.description" />
<div>
<button onClick={() => setLocale("en")}>English</button>
<button onClick={() => setLocale("es")}>Español</button>
<button onClick={() => setLocale("fr")}>Français</button>
</div>
</IntlProvider>
);
}
When we click on a certain button, the locale
state of our app updates to the selected locale, triggering a re-render of the IntlProvider
component with the new locale
value and data. Consequently, the text in our application that’s managed by the react-intl
library updates to reflect the chosen language.
You can see the running application in the following CodeSandbox link.
The user now has a dynamic way to switch between different languages without having to refresh the entire page!
In today’s article, we showed a simple example of introducing internationalization to a React application. There are are more advanced features to explore in react-intl, including handling more complex string translations, number formatting, date and time formatting, and handling plurals and genders, which are important aspects in many languages.
Building an app with internationalization in mind from the outset is generally easier than trying to retrofit it later. So even if you’re only targeting a single language region to begin with, it’s worth considering from the start.
Looking to build with i18n baked into your components? The Internationalization package from Progress KendoReact applies the desired cultures by providing services and pipes for the parsing and formatting of dates and numbers.]]>
Sometimes we want to access the value of a previous prop or state value. React does not provide a built-in way to do this, so let’s explore how to do this with refs.
In React, refs are non-reactive values in components and hooks. We can use this to store the values of things that we want to last between re-renders and won’t trigger a re-render when their values change.
To define a ref, we can use the useRef
hook.
For instance, we write:
import { useRef } from "react";
export default function App() {
const ref = useRef();
ref.current = 100;
console.log(ref.current);
return <div className="App"></div>;
}
to define the ref
ref with the React built in useRef
hook.
And we set its value by setting ref.current
to 100. We can put the ref.current
value assignment in the root level of the component since it won’t trigger a re-render of the component when we assign a value to it. And the value will be kept as long as the component is mounted.
These characteristics of refs make them good for keeping the previous values of React reactive values.
To use refs to store the previous values of states, we can assign those values to refs. For instance, we write:
import { useRef, useState } from "react";
export default function App() {
const prevCountRef = useRef();
const [count, setCount] = useState(0);
const onClick = () => {
prevCountRef.current = count;
setCount((c) => c + 1);
};
console.log(prevCountRef.current, count);
return (
<div className="App">
<button onClick={onClick}>increment</button>
<div>{count}</div>
</div>
);
}
to define the count
state with the useState
hook. And we define the prevCountRef
ref with the useRef
hook.
Next, we define the onClick
function that sets the prevCountRef
to the count
value before we call setCount
to increment the count
state value by 1.
Then we add a button to call onClick
when we click on the button to update both values. As a result, we see from the console log that prevCountRef.current
is always the previous count
value and count
having the latest value.
Likewise, we can store the previous values of props the same way since props are also reactive values.
To do the same thing with props, we write:
import { useEffect, useRef, useState } from "react";
const CountDisplay = ({ count }) => {
const prevCountRef = useRef();
useEffect(() => {
prevCountRef.current = count;
}, [count]);
console.log(prevCountRef.current, count);
return <div>{count}</div>;
};
export default function App() {
const [count, setCount] = useState(0);
const onClick = () => {
setCount((c) => c + 1);
};
return (
<div className="App">
<button onClick={onClick}>increment</button>
<CountDisplay count={count} />
</div>
);
}
to define the CountDisplay
component.
In it, we take the count
prop and assign it as the value of the prevCountRef
value as the count
value changes.
We can do that by putting prevCountRef.current = count;
in the useEffect
hook callback and make the hook watch the count
value.
This can keep the previous count
prop value since it won’t trigger a re-render until the count
prop gets updated.
As a result, we see from the console log that prevCountRef.current
is always the previous count
value and count
having the latest value as we do with the previous example.
To make the logic we have for storing previous props or states reusable, we can move the logic into its own hook.
To do this, we make the hook accept the reactive prop or state value and return the previous and latest reactive variable values.
For instance, we can write our own hook by writing:
import { useEffect, useRef, useState } from "react";
const usePrevious = (value) => {
const prevValueRef = useRef();
useEffect(() => {
prevValueRef.current = value;
}, [value]);
return { prev: prevValueRef.current, current: value };
};
export default function App() {
const [count, setCount] = useState(0);
const { prev, current } = usePrevious(count);
const onClick = () => {
setCount((c) => c + 1);
};
console.log(prev, current);
return (
<div className="App">
<button onClick={onClick}>increment</button>
<div>{count}</div>
</div>
);
}
to define the usePrevious
hook. In it, we just take the logic we previously had and put it in the hook function.
The only difference is that the hook takes the value
parameter where value
is the reactive value. And the hook function returns an object with the prev
and current
values that are derived from the same logic as before.
We use the usePrevious
hook by passing the count
state into it and take the prev
and current
properties from the object is returned.
Likewise, we can use the usePrevious
hook to store the previous value of props by writing:
import { useEffect, useRef, useState } from "react";
const usePrevious = (value) => {
const prevValueRef = useRef();
useEffect(() => {
prevValueRef.current = value;
}, [value]);
return { prev: prevValueRef.current, current: value };
};
const CountDisplay = ({ count }) => {
const { prev, current } = usePrevious(count);
console.log(prev, current);
return <div>{count}</div>;
};
export default function App() {
const [count, setCount] = useState(0);
const onClick = () => {
setCount((c) => c + 1);
};
return (
<div className="App">
<button onClick={onClick}>increment</button>
<CountDisplay count={count} />
</div>
);
}
We just call the usePrevious
with the count
prop in the CountDisplay
component instead of in the App
component.
Either way, we get the same result.
We can use refs to store the previous values of state and props since refs don’t trigger the re-rendering of components when their values change.
]]>React Server Components (RSCs) aren’t brand new—in fact, they’ve been around for a few years now. So why are they still so misunderstood?
Like many React devs right now, I had a general understanding of what they were, but couldn’t have given you the details on how they work or why you might want to use them. As I’ve written about previously, I’m not generally an early-adopter—I like to wait for a little bit and see how the practical use case of new technologies pan out. That being said, the future of React certainly seems to be trending toward server components; it was time for me to get up to speed.
Because Progress KendoReact prioritizes always supporting the newest and latest, we’ve implemented RSCs in our own components. And they’re certainly a hot discussion topic right now, with many of the big voices in React really excited about them and confident that they’re the right direction for React to be heading … but that hype didn’t seem to be translating directly to major adoption in the community (myself, admittedly, included). That got me curious—not only about where that adoption gap was coming from, but also about what I might have been missing out on by not jumping on board sooner.
If you were like me—hanging out on the sidelines of the RSC movement, keeping an eye on stuff but not necessarily testing the waters yourself—you’re in the right place. Let’s take a look at the big picture regarding server components: not only what they are, but also why they didn’t take off immediately and the pros and cons of incorporating them now.
Within React, there are now two types of components: server components and client components. Historically, React exclusively used client components, so it might make the most sense to start there.
The “client” in client components refer to the fact they they render and function entirely in the user’s browser—otherwise known as the client slide. Server-side and client-side refer to different places where the heavy lifting (if you will) is carried out in an application. Server-side stuff happens on the web server; client-side stuff happens on the client’s (aka user’s) device.
So when we wrote client components in React, it was with the assumption that all the functions, rendering, state management, etc. would happen exclusively in the browser on the user’s laptop/phone/tablet/screen on the front of their smart fridge/whatever.
Right. So, if client components do all their ish on the browser (aka the client side), then we can extrapolate that server components will do all their ish on the web server.
Traditionally, with client components, the component would send a request and wait for a response. That response would either be the data that we requested … or an error. If we got back the data, then that could be passed down to child components via props and rendered in the UI for the user. If we got back an error, well … we show the user an error message (hopefully).
In the meantime, it makes for a lot of waiting on the user’s side—a common complaint in modern app development. While that can be mitigated by lazy-loading, skeleton UIs, etc., those were still just bandages over the real problem—that apps are only getting bigger, and users aren’t always loading them with lightning-fast connections.
Server components run entirely on the server, which eliminates that back-and-forth game. The flip side is that their code won’t get included in the JS bundle—that’s a huge chunk of why the load times are way faster, but it also means that they can’t re-render when state changes. They do their thing on the server and then they’re done—that rendered component gets sent to the client side as-is, as HTML.
This means that any React app making use of RSCs would need a combination of server components and client components in order to be fully reactive to user inputs. Generally, you’ll want to reach for a server component when you need to do heavy data fetching or deal with big dependencies, then leverage client components when you need user interaction and stateful UI. This often looks like a parent-child relationship where RSCs are the parent and client components are the child. The Next.js docs have a really great breakdown of the different use cases where you might reach for a server component vs. a client component.
Almost certainly. From a technical perspective: We know they’ll be included as part of the mainline, stable release in React 19 (although we don’t know when exactly that will come). From an adoption perspective: In addition to the endorsement of RSCs from several big voices in the React community, our most tangible indication of RSCs gaining popularity is Next.js.
Next.js is currently the only framework recommended in the official React docs that supports server components. So, why is that?
When the new React docs were officially moved out of beta in March 2023, it came with a big shift—frameworks are now the officially recommended approach for writing new React apps. Previously, frameworks were mentioned alongside a “roll your own” approach, with both being suggested as equally recommended approaches for new apps. Now, the docs specifically include several paragraphs on why using React without a framework is not the ideal approach.
One of the most popular React frameworks available (and the top-listed recommendation in the official React docs) is Next.js. Vercel, the company who maintains Next.js, has a close relationship with the React team—some have even argued too close. Setting aside whether it should be that way or not, that close relationship means that the decisions Next.js makes are closely interwoven with the decisions that React makes (and vice versa).
When Next.js takes an opinionated approach to something in their framework, it kind of comes with the tacit endorsement of the React team. In fact, Next.js is even built on the Canary (or early-release) versions of React instead of the Stable releases. While you can, without a doubt, still use React without Next.js, it’s getting hard to argue that they’re not becoming more and more enmeshed. Whether or not that’s the right move for the future of React is a different question—one I’m sure you can find several different hot takes on if you poke around, but which we won’t cover in this particular article.
That was a lot of context just to say: the choices that Vercel makes with regards to Next.js carry significant weight in the React world—and in version 13, Next.js made server components the default approach when spinning up a new app.
Developers can still, of course, opt out of this when using Next.js, but it takes a little config. Considering the amount of influence that Next.js has within the React community, it seems pretty darn likely that if Next.js is betting on RSCs, RSCs are gonna happen.
There are a few contributing factors:
As of time of this writing, there is no guide to React Server Components currently included in the official React documentation. There are several mentions regarding the development of RSCs, plans for the future, etc., but no actual information about how and when they should be used. The most recent informational update about RSCs in the official docs is in a February 15th “React Labs” blog post, where they confirm that RSCs will be included in the next major version of React (but the actual release date of React 19 is still unknown).
There are some good guides out there to help new users really wrap their heads around server components—I especially liked React Server Components: A comprehensive guide by Chinwike Maduabuchi and Making Sense of React Server Components by Josh Comeau. Dan Abramov has also done a fair bit of tweeting about RSCs, offering guidance in a less-official capacity.
However, I would argue none of that is quite the same as real documentation.
Currently, server component implementation options are fairly limited—by which I mean that they need to be used in conjunction with a framework. That previously mentioned React Labs blog on RSC development even says directly:
Building your own RSC-compatible framework is not as easy as we’d like it to be, mainly due to the deep bundler integration needed.
So, while it’s technically possible, RSCs without a framework are currently more in the realm of weekend tinkering vs. real business-case applications.
The other piece of important information to acknowledge here is that when we say RSCs need a framework, “framework” effectively just means “Next.js.” There are some smaller frameworks (like Waku) that support RSCs. There are also some larger and more established frameworks (like Redwood) that have plans to support RSCs or (like Gatsby) only support RSCs in beta. However, Next.js is currently the only framework recommended in the official React docs that supports server components.
So, if you didn’t happen to already be using Next.js for your React app, then your options for leveraging RSCs are pretty slim: either migrating to a new framework (which is no small task) or waiting for support in the framework you’re already invested in. As cool as RSCs sound, I can tell you which one I’d be doing (spoiler alert: it’s the waiting).
The boring, logical answer to this is (like so many things in web development): “It depends.” It depends on your team, your current tech stack, whether you’re starting a new project or this would mean refactoring a legacy app, etc.
My personal opinion is that if I were in the lucky position to be starting a new project right now, I would probably do so with Next.js—and with server components. In addition to just being a helpful thing you can use to improve performance, I think there’s some real truth that the wind definitely seems to be blowing in the direction of RSCs—and that’s not a thing I would want to get stuck fighting against in the next few years.
However, if I were the maintainer of a legacy app, I wouldn’t make the jump just yet. I don’t think the support for RSCs are robust enough at this point to validate making the switch in an existing project. Would there be benefits? Sure. Would those benefits be worth investing such a chunk of time and effort into (what will almost certainly be) a huge refactoring project? My gut says no.
For many years, client components were the backbone of the React experience—and for the most part, it worked pretty well. To be completely fair, it still works pretty well; unless you’re experiencing pain points specifically related to some of those common pitfalls of client components, then there’s really nothing that says you have to stop writing React apps using this pattern. I think it’s important to note that—unlike the switch from class components to functional components—new developers are not (at the point of writing this article) actively being discouraged from writing entirely client-side apps. In fact, apps leveraging RSCs are (again, at time of this writing) significantly less common than apps that use client components.
While it’s a safe bet that RSCs are in our future, we’re still a ways away from seeing them become officially “stable” in React. In the latest version (18.2.0), they are still clearly marked as “experimental.” While we do know that they’ll be included as part of the stable release in React 19, we don’t know when that will be—and even once that happens, we’ll still see a lag between stability and adoption as frameworks, libraries and regular ol’ devs like you and me start to figure out what realistic incorporation looks like.
While some frameworks and libraries are getting ahead of the curve on this, it’s still nothing that could be considered widespread adoption. That means that true standardization of the RSC approach, figuring out good and bad patterns, and inclusion in the official docs are still—most likely—years away.
Whenever you decide to check it out, know that KendoReact has already blazed the trail. Our components will be there to make building your React apps easier, faster and more accessible—no matter what you choose!
]]>In the realm of web development, the way we handle navigation and URL changes plays a pivotal role in user experience, performance and search engine optimization. Understanding the difference between server-side and client-side routing is fundamental for developers. In this article, we’ll delve into the specifics of these two popular routing techniques and compare their pros and cons.
First and foremost: let’s define routing for those who may be new to the concept.
In web development, routing often refers to splitting an application’s UI based on rules derived from the browser URL. Imagine clicking a link and having the URL go from https://website.com
to https://website.com/about/
.
That’s routing.
When we visit the /
path of a website, we intend to visit the home route of that website. If we visit /about
, we want to render the About page, and so on.
Many applications can technically be written without routing but this can get messy as an application grows. Defining routes in an application is useful since one can separate different areas of an app and protect areas of the app based on certain rules.
Routing is often categorized into two main buckets:
In a server-driven application, requests to a URL often follow a pattern:
Server-side routing is often set up to retrieve and return different information depending on the incoming URL. Writing server-side routes with Express.js generally looks like the following:
const express = require("express");
const router = express.Router();
// define the about route
router.get("/about", function (req, res) {
res.send("About us");
});
Or using Ruby on Rails, a similar route definition might look like this:
# routes.rb
get '/about', to: 'pages#about'
# PagesController.rb
class PagesController < ActionController::Base
def about
render
end
end
Whether it’s Express.js, Ruby on Rails, or any other server-side framework, the pattern often remains the same. The server accepts a request and routes it to a controller, and the controller runs a specific action (e.g., returns specific information), depending on the path and parameters.
In applications employing client-side routing, the server initially provides a single HTML file, irrespective of the URL path. This “shell” is then enriched and manipulated by JavaScript running in the browser. Subsequent navigation between different parts of the app does not send new requests to the server but rather modifies the displayed content based on the loaded JavaScript and data.
These are characteristic of single-page applications (SPAs)—web apps that load only once (i.e., the server provides a single template) and JavaScript is used to dynamically render different pages.
The flow for client-side routing usually looks like the following:
With client-side routing, while the initial load might seem slower because the entire app (or large parts of it) gets loaded, subsequent navigations are swift and seamless, as they don’t require round-trips to the server.
Client-side routing can be implemented using various libraries and frameworks. For instance, React Router is a popular client-side routing library for React applications:
import * as React from "react";
import { createRoot } from "react-dom/client";
import {
createBrowserRouter,
RouterProvider,
Route,
Link,
} from "react-router-dom";
const router = createBrowserRouter([
{
path: "/",
element: (
<div>
<h1>Hello World</h1>
<Link to="about">About Us</Link>
</div>
),
},
{
path: "about",
element: <div>About</div>,
},
]);
createRoot(document.getElementById("root")).render(
<RouterProvider router={router} />
);
While for Vue applications, Vue Router is the recommended client-side routing library.
const Home = { template: "<div>Home</div>" };
const About = { template: "<div>About</div>" };
const routes = [
{ path: "/", component: Home },
{ path: "/about", component: About },
];
const router = VueRouter.createRouter({
history: VueRouter.createWebHashHistory(),
routes,
});
Both server-side and client-side routing have their places in modern web development. The choice between them often comes down to the specific needs of the project.
In server-side routing:
While in client-side routing:
If search engine optimization and initial page load speed are critical, server-side routing might be the way to go. On the other hand, if the priority is to provide a dynamic, app-like user experience, then client-side routing in a SPA could be the ideal choice.
In today’s versatile tech landscape, hybrid solutions are also emerging that combine the best of both worlds, such as Next.js for React or Nuxt.js for Vue, which offer server-side rendering for SPAs, thus addressing many traditional drawbacks of client-side routing. We’ll talk about these frameworks in more detail in upcoming articles!
Ultimately, understanding the nuances, strengths and weaknesses of both the server-side and client-side routing methods will empower you to make informed decisions that best suit your application’s needs.
]]>With the publication of the updated React docs last year, the use of frameworks—with Next.js at the top of the list—is the official recommendation of the React team when it comes to getting a new project up and going.
I hadn’t had a chance to explore Next.js in much detail, so I decided to knock out two birds with one stone—rework my personal website (always on a developer’s to-do list, right?) and get up-to-speed with this new suggested approach for React projects. Of course, I also leveraged the Progress KendoReact library and ThemeBuilder for my components and styling, because—why not? Quick, easy, accessible: what’s not to love?
The Next.js ecosystem really favors CSS modules, with built-in support simply by including the module.css
extension in your file names. They also have built-in support for Sass, after a quick npm install
. Since I wanted to check out the “standard” Next.js experience, I went ahead and followed their recommendations, making use of both. Then, I installed my KendoReact components and set up my license.
Since I was just working on my personal website (not a super-complex, full-featured application), I didn’t really need a proper design system—I just made a couple quick tweaks to the typography and color scheme of the Kendo Default theme in the ThemeBuilder and called it good!
I knew I wanted my site to make use of the prefers-color-scheme
media query, so it would be able to swap automatically between light and dark mode to match the user’s system settings. However, because my design was so simple, I didn’t bother creating two entirely different themes for this—I didn’t want to mess with switching between stylesheets just to swap a couple hex codes.
Instead, I added extra custom variables to my ThemeBuilder project: dark-mode variables for body background, component background, text color and accessibility-adjusted versions of my two main colors (to ensure I always had high enough color contrast). So, in total, five extra custom variables got exported to my _tokens.scss
file along with the standard ones for the Default theme.
ThemeBuilder offers exports in vanilla CSS, as well as Sass; in this case, I already knew I wanted to use the Sass ones. That meant that the first thing I needed to sort out was where to place my ThemeBuilder-exported Sass files. In Next.js, that required adding global styles, in addition to the default CSS module scoped styles.
Because I was using the most recent stable version of Next.js (14.0, when this blog was written), that meant that the App Router was the default structure—as opposed to the Pages Router used in Next.js 13.3 and earlier. As it turns out, this makes a slight difference when it comes to where your global styles will live. I’ll cover both ways—how I did it in the current Next.js version using App Router, and how you might need to configure things for a legacy application using the Page Router.
If you’re using this more recent approach, then global styles can be imported into any layout, page or component within the app
directory. For clarity and easy searchability, I renamed my ThemeBuilder-exported index.scss
file to global.scss
. Then, I imported it into the root layout file—app/layout.tsx
(or app/layout.js
, if you’re not using TypeScript).
If you’re using this older approach, you’ll need to create a new file called _app.js
within the pages
directory and include this code:
import '../styles/global.scss';
export default function App({ Component, pageProps }) {
return <Component {...pageProps} />;
}
Then, restart your dev server and you should be good to go! Just remember, this file is the only place where global styles can be imported using the Pages Router!
Since I knew I wanted a super simple dark mode (as mentioned before) I made a short addition to the end of my global.scss
file to include the prefers-color-scheme
media query. There, I used the custom variables I created in ThemeBuilder, imported from the _tokens.scss
file, to redefine a couple key styles. In my actual project, there were a few more than this, but you get the idea.
@media (prefers-color-scheme: dark) {
body {
background-color: $tb-kendo-body-bg-dark;
}
a {
color: $tb-kendo-accessible-link-text;
}
}
If I’m completely honest, sometimes I struggle to write blogs like these because it feels like there’s not much to say—everything just works! In this case, when I first started the project, I was a little nervous about how easily the exported ThemeBuilder stylesheets would work with the Next.js structure. I had heard from other developers that Next.js was highly opinionated, and I had a little hesitation about how much adjustment might be required for these two technologies to work and play well together.
My conclusion is this: While it’s true that Next.js is opinionated, they also built in solutions for the main situations where developers were most likely to need something outside the default configuration. Next.js doesn’t include global style support out of the box, so you can’t literally drag and drop the ThemeBuilder files into the app folder—the way you can with a CRA app. However, getting global styles set up in Next.js was well-documented and not terribly challenging. Once that was in place, we were pretty much back into the drag-and-drop zone when it came to implementation.
This will almost certainly be my approach for new app development moving forward—especially now that KendoReact is rolling out new Server Components with RSC architecture, which will be right at home in Next.js! And, of course, if you aren’t already using KendoReact, you can try it out—in combination with your framework of choice—completely free for 30 days.
]]>If you’re a React developer, you’re probably familiar with hooks. And if you’re a meticulous developer, you want to ensure your code is optimized for performance. That’s
where the useMemo
hook comes in.
In this article, we’ll explore the useMemo
hook and show you how to use it effectively to boost the performance of your React application.
React Hooks were introduced in React 16.8. It was one of the most essential releases of React, and it changed the way we build modern applications. Being able to manage state inside functional components was brilliant, making our applications more readable, scalable, and reusable. We could create custom hooks usable across different components.
Of course, this was a while ago. The community has since been evolving and creating new things.
Several built-in hooks are available in React, including useState
, useMemo
, useEffect
and useCallback
. Each hook serves a specific purpose and can be used to enhance
the functionality of your components.
The useState
hook is the simplest—it’s used to manage the state of functional components. You’ve worked with it so often that we don’t have to say much about it, so l et’s talk about the three hooks that scare
people and make their lives harder.
The useEffect
, useMemo
and useCallback
hooks are considered more complex to understand and use than other hooks because they are built to solve more complex needs.
We don’t have time to discuss all three in this article, but we will dive deep into the useMemo
hook specifically.
The useMemo
hook is built into React, allowing you to memoize a value. In other words, useMemo
caches the result of a function and returns the cached value whenever it’s called again with the same arguments. Caching
optimizes performance and reduces unnecessary computations, which is particularly useful when you have expensive computations that are triggered frequently.
Here’s how you can use useMemo
:
useMemo
hook.useMemo
.const cachedValue = useMemo(calculateValue, dependencies)
One thing to remember with useMemo
is that it’s only sometimes necessary. If the computation is not expensive or doesn’t need to be re-computed frequently, you might not need to use this hook. Using useMemo
unnecessarily
can hurt performance, as it adds overhead to your component.
Another thing to remember is that useMemo
only memoizes the result of the function. If the function has side effects (i.e., it modifies state or interacts with the DOM), they will still occur every time the function is called, even if the
result is memoized. In those cases, you should use the useCallback
hook instead, which memoizes the entire function (including its side effects).
While useMemo
can be a powerful tool, it’s essential to use it correctly. Here are some common mistakes to avoid:
If you include unnecessary dependencies, the memoized value will be recomputed unnecessarily, which can hurt performance. Make sure to memoize only the dependencies that affect the value.
import React, { useMemo } from 'react';
const UserComponent = ({ firstName, lastName }) => {
const fullName = useMemo(() => {
return `${firstName} ${lastName}`;
}, [firstName, lastName]);
return <div>{fullName}</div>;
};
export default UserComponent;
Whenever you use React hooks with an array of dependencies (not exclusive for useMemo
), it’s essential to include the dependencies that directly impact the memoized value. In this case, you should only have [firstName, lastName]
in the dependency array to avoid unnecessary recomputations when irrelevant values change.
If a value is large or complex, memoizing it can decrease performance rather than improve it. This is because the useMemo
hook must compute and cache the value, which can be time-consuming.
import React, { useMemo } from 'react';
const UserDataComponent = ({ data }) => {
const processedData = useMemo(() => {
// Perform some heavy computations or transformations on user data
// ...
return processedData;
}, [data]);
return <div>{processedData}</div>;
};
export default UserDataComponent;
Imagine that the data prop is extensive, an object with multiple objects and many other structures inside. In that case, this caching process can take significant time and resources, potentially decreasing performance rather than improving it.
When the value being memoized is large or involves complex computations, we should remember that memoization is most effective when applied to values that are expensive to compute but have relatively stable dependencies.
Although useMemo
can be helpful, it shouldn’t be used excessively. Only use it in cases where you have a computationally expensive function that needs to be memoized.
It’s important to note that useMemo
is not a silver bullet for optimizing performance in your React application. While it can be helpful in some instances, overusing it can lead to decreased performance. It’s essential to use
useMemo judiciously and only when necessary.
It’s also essential to consider which dependencies to include in the array carefully. Including a dependency that doesn’t affect the memoized value can lead to unnecessary re–renders and decreased performance.
Now that you understand how useMemo
works, let’s look at a few examples of how you can use it in your code.
Consider a scenario where you have a long list of items to filter based on a search query. When the user types in the search query, you want to show only the items that match the query. However, computing the filtered list every time the user types in
a new character can be expensive. This is where useMemo
can help.
import React, { useState, useMemo } from 'react';
const FiltersComponent = ({ items, filters }) => {
const [query, setQuery] = useState('');
const filteredItems = useMemo(() => {
const filtered = items.filter((item) =>
item.toLowerCase().includes(query.toLowerCase())
);
return filtered;
}, [items, query, filters]);
const handleSearchChange = (event) => {
setQuery(event.target.value);
};
return (
<div>
<input type="text" value={searchQuery} onChange={handleSearchChange} />
<ul>
{filteredItems.map((item) => (
<li key={item}>{item}</li>
))}
</ul>
</div>
);
};
export default FiltersComponent;
In this example, the filteredItems
array is memoized using useMemo
. The function depends on items and filters, so they’re included in the dependency array. This way, the filteredItems
array is only recomputed
when items or filters change. This can help improve the performance of your application, especially when dealing with large lists.
There are times when you need to perform a computation that is expensive and time-consuming. For example, you might need to perform a complex calculation or iterate over a large dataset. In such cases, memoizing the result can help improve the performance of your application.
import React, { useMemo } from 'react';
const ExpensiveResultComponent = () => {
const expensiveResult = useMemo(() => {
let result = 0;
for (let i = 0; i < 1000000000; i++) {
result += i;
}
return result;
}, []);
return <div>{expensiveResult}</div>;
};
export default ExpensiveResultComponent;
In the code example provided, the expensiveResult
is memoized using useMemo
. The function computes a value using a loop that iterates one billion times. By memoizing the result, you can avoid recomputing the value every time
the component re-renders. This can help improve the performance of your application, especially when dealing with computationally intensive tasks.
Overall, useMemo
is a powerful tool that can help you improve the performance of your React application. By memoizing expensive computations or filtered arrays, you can avoid unnecessary rerenders and enhance the responsiveness of your application.
However, it’s essential to use useMemo
correctly and avoid common mistakes that can hurt performance. Hopefully, this article has provided you with a solid understanding of how to use useMemo
once and for all.
See how user interfaces are benefitting from LLMs and how you can start to implement them into your Blazor, Angular, React or Vue app.
The rise of Large Language Models (LLMs) in tech is reshaping UI/UX development. LLMs are becoming key in creating user-friendly interfaces that mimic human communication, making digital experiences more intuitive and responsive. This trend points toward a future where digital interfaces are more than just tools—they’re evolving into entities that understand and interact naturally with users, thereby enhancing the digital experience.
Throughout this blog series, we’ll be highlighting different ways to integrate LLMs with UIs using Telerik and Kendo UI components, and provide practical insights for developers and designers, demonstrating how to achieve efficient and user-friendly AI application interfaces with minimal effort.
In the evolving world of AI Large Language Models (LLMs), UI design is crucial for enhancing user experience. Let’s explore some of the common UI/UX approaches that are currently shaping how we interact with AI-driven tools.
AI apps like ChatGPT and Bard function as standalone websites, featuring a central input bar for user queries. This layout largely borrows from established web and mobile UI/UX designs, reflecting a familiar structure that users can navigate easily.
ChatGPT
Such GenAI UI implementations excel in providing a responsive interface, ideal for reading and navigating through extensive text, offering a focused and comprehensive search experience. On the other hand, this approach is not suitable when you want to integrate GenAI as part of a larger context of a purpose built application.
Bard
Gen AI models are also being integrated as taskbars within websites, for example as seen with notion.so. These taskbars remain discreet until activated by user commands or shortcuts.
Notion
This approach doesn’t dominate the user interface but instead remains subtly integrated in a larger application context. This design choice ensures that the AI features don’t disrupt the user’s workflow or distract from the primary functions of your website.
The use of widgets is an emerging trend, distinguishing AI LLMs from traditional chatbots.
GrammarlyGo exemplifies this, offering specialized AI-enhanced prompts and commands. Positioned for convenience, these widgets aim to be user-friendly, though they sometimes face limitations in space, especially with longer responses.
Grammarly
Some AI applications are merging these approaches for a comprehensive UI/UX experience. A good example is Microsoft Copilot, which integrates a chat widget for generating prompts, a taskbar for specific commands and a landing page for content creation, encapsulating the benefits of each approach.
Copilot
As we navigate through these varied UI designs, it’s clear that the focus is on creating intuitive, efficient and versatile interfaces. These developments are steering us toward a future where AI is seamlessly woven into our digital interactions, enhancing both functionality and user engagement.
While the current state of UI/UX design for Gen AI is functional, it often lacks the ease and fluidity necessary for optimal user experience, creating friction that we are committed to eliminating.
Our goal at Progress is simple yet profound: to reduce the clutter of visual interfaces and enable users to execute tasks through direct, intuitive AI commands, aligning digital experiences more closely with natural human behavior.
Introducing the new AI Prompt Component within our component library, devised to streamline the integration of your favorite AI services into applications built with Kendo UI for Angular, KendoReact, Kendo UI for Vue and Telerik UI for Blazor.
These features aim to provide an efficient user interface for interacting with AI services, focusing on enhancing usability while offering developers flexibility in customization.
Select an option to see the code block:
Blazor Code
<TelerikAIPrompt OnPromptRequest="@HandlePromptRequest" OnCommandExecute="@HandleCommandExecute" Commands="@PromptCommands">
</TelerikAIPrompt>
@code {
public List<AIPromptCommandDescriptor> PromptCommands { get; set; } = new List<AIPromptCommandDescriptor>()
{
new AIPromptCommandDescriptor() { Id = "1", Title = "Correct Spelling and grammar", Icon = nameof(SvgIcon.SpellChecker)},
new AIPromptCommandDescriptor()
{
Id = "2",
Title = "Change Tone",
Icon = nameof(SvgIcon.TellAFriend),
Children = new List<AIPromptCommandDescriptor>
{
new AIPromptCommandDescriptor() { Id = "3", Title = "Professional" },
new AIPromptCommandDescriptor() { Id = "4", Title = "Conversational" },
new AIPromptCommandDescriptor() { Id = "5", Title = "Humorous" },
new AIPromptCommandDescriptor() { Id = "6", Title = "Empathic" },
new AIPromptCommandDescriptor() { Id = "7", Title = "Academic" },
}
},
};
public void HandlePromptRequest(AIPromptPromptRequestEventArgs args)
{
// execute call to LLM API
// args.Output = responseMessage;
}
public void HandleCommandExecute(AIPromptCommandExecuteEventArgs args)
{
// execute command
// args.Output = responseMessage;
}
}
Angular Code
import { PromptCommand, CommandExecuteEvent, PromptOutput, PromptRequestEvent } from '@progress/kendo-angular-conversational-ui';
import { spellCheckerIcon, tellAFriendIcon } from '@progress/kendo-svg-icons';
@Component({
selector: 'my-app',
template: `
<kendo-aiprompt [style.width.px]="300"
[(activeView)]="activeView"
[promptOutputs]="promptOutputs"
[promptCommands]="promptCommands"
(promptRequest)="onPromptRequest($event)"
(commandExecute)="onCommandExecute($event)"
>
<kendo-aiprompt-prompt-view></kendo-aiprompt-prompt-view>
<kendo-aiprompt-output-view></kendo-aiprompt-output-view>
<kendo-aiprompt-command-view></kendo-aiprompt-command-view>
</kendo-aiprompt>
`
})
export class AppComponent {
public promptCommands: Array<PromptCommand> = [{
id: 1,
text: "Correct Spelling and grammar",
svgIcon: spellCheckerIcon
}, {
id: 2,
text: "Change Tone - Professional",
svgIcon: tellAFriendIcon
}, {
id: 3,
text: "Change Tone - Conversational",
svgIcon: tellAFriendIcon
}, {
id: 4,
text: "Change Tone - Humorous",
svgIcon: tellAFriendIcon
}, {
id: 5,
text: "Change Tone - Empathic",
svgIcon: tellAFriendIcon
}, {
id: 6,
text: "Change Tone - Academic",
svgIcon: tellAFriendIcon
}
];
public activeView: number = 0;
public promptOutputs: Array<PromptOutput> = [];
lkl
public onPromptRequest(ev: PromptRequestEvent): void {
// execute call to LLM API
// populate the outputs collection
// switch to the output view
}
public onCommandExecute(ev: CommandExecuteEvent): void {
// execute command
// populate the outputs collection
// switch to the output view
}
}
React Code
const AIPromptComponent = () => {
// State for managing active view and outputs
const [activeView, setActiveView] = useState(0);
const [outputs, setOutputs] = useState([]);
// Handle change in active view
const handleActiveViewChange = (newView) => {
setActiveView(newView);
};
// Handle the prompt request
const handleGenerate = (event) => {
// execute call to LLM API
// populate the outputs collection
// switch to the output view
};
// Handle command execution
const handleCommandExecute = (event) => {
// execute command based on the event details
// populate the outputs collection
// switch to the output view
};
return (
<AIPrompt
activeView={activeView}
onActiveViewChange={handleActiveViewChange}
onPromptRequest={handleGenerate}
onCommandExecute={handleCommandExecute}
toolbarItems={[promptView, outputView, commandsView]}
>
<AIPromptView />
<AIPromptOutputView outputs={outputs}/>
<AIPromptCommandsView
commands={[{
id: '1',
title: "Correct Spelling and grammar",
icon: spellCheckerIcon as any
}, {
id: '2',
title: "Change Tone",
icon: tellAFriendIcon as any,
children: [
{ id: "3", title: "Professional" },
{ id: "4", title: "Conversational" },
{ id: "5", title: "Humorous" },
{ id: "6", title: "Empathic" },
{ id: "7", title: "Academic" }
]
}]}
/>
</AIPrompt>
);
};
export default AIPromptComponent;
Progress is aiming for seamless technology interaction with the integration of LLMs in UI development. As AI continues to advance, we’re committed to adapting our UI components to stay at the forefront of digital interaction trends.
We’re eager to hear from you—tell us what you would like to see from us in regards to AI and UI components in the comments or in the Feedback Portal.
Handling events and managing time effectively are essential elements for any app dealing with appointments, meetings or deadlines. However, building this functionality from scratch can be a daunting task, requiring work needed to surface various components like calendar views, user interaction interfaces and synchronization with external data sources.
This is where Progress KendoReact’s Scheduler component comes in. This React Scheduler component offers an intuitive and flexible solution for creating well-organized schedules in our React applications. In this article, we’ll spend some time exploring the key features of the KendoReact Scheduler component and how they can help us create customizable scheduling interfaces easily.
The KendoReact Scheduler component is distributed through the kendo-react-scheduler NPM package and can be imported directly from this package. While installing the Scheduler component package, it’s necessary to install all supporting packages/dependencies that will potentially be used within the scheduler UI elements so that the Scheduler loads and works correctly.
npm install --save @progress/kendo-react-scheduler @progress/kendo-react-intl @progres/kendo-react-popup @progress/kendo-react-dialogs @progress/kendo-react-dateinputs @progress/kendo-react-dropdowns @progress/kendo-react-inputs @progress/kendo-react-buttons @progress/kendo-date-math @progress/kendo-react-form @progress/kendo-licensing @progress/kendo-svg-icons
Once we’ve installed the necessary packages, we can start integrating the Scheduler components into our project. Before we do this, we’ll first create an events-utc.js
file that will contain the data for the events we want to be displayed in our scheduler.
const baseData = [
{
TaskID: 4,
OwnerID: 2,
Title: "Bowling tournament",
Start: "2023-06-24T16:00:00.000Z",
End: "2023-06-24T19:00:00.000Z",
isAllDay: false,
},
{
TaskID: 5,
OwnerID: 2,
Title: "Take the dog to the vet",
Start: "2023-06-24T12:00:00.000Z",
End: "2023-06-24T13:00:00.000Z",
isAllDay: false,
},
{
TaskID: 6,
OwnerID: 2,
Title: "Call Charlie about the project",
Start: "2023-06-21T11:30:00.000Z",
End: "2023-06-21T13:00:00.000Z",
isAllDay: false,
},
{
TaskID: 7,
OwnerID: 3,
Title: "Meeting with Alex",
Start: "2023-06-22T14:00:00.000Z",
End: "2023-06-22T16:00:00.000Z",
isAllDay: false,
},
{
TaskID: 9,
OwnerID: 2,
Title: "Alex's Birthday",
Start: "2023-06-18T13:00:00.000Z",
End: "2023-06-18T14:00:00.000Z",
isAllDay: true,
},
];
In the above code, we’ve created an array of objects, each representing an event or task. The fields declared in each object contain some information about the event or task such as its title, its start and end times, and an isAllDay
property to dictate if the event is to be a full-day event.
In our events-utc.js
file, we’ll also export a displayDate
that will be used as the initial default date in our calendar in addition to a sampleData
array that will be the sample array data we’ll feed into our scheduler components.
In the calendar examples we create in this article, we’ll have all our events and tasks arbitrarily scheduled for June 2023 so we’ll have our displayDate
value be June 24, 2023, at midnight UTC.
const baseData = [
{
TaskID: 4,
OwnerID: 2,
Title: "Bowling tournament",
Start: "2023-06-24T16:00:00.000Z",
End: "2023-06-24T19:00:00.000Z",
isAllDay: false,
},
{
TaskID: 5,
OwnerID: 2,
Title: "Take the dog to the vet",
Start: "2023-06-24T12:00:00.000Z",
End: "2023-06-24T13:00:00.000Z",
isAllDay: false,
},
{
TaskID: 6,
OwnerID: 2,
Title: "Call Charlie about the project",
Start: "2023-06-21T11:30:00.000Z",
End: "2023-06-21T13:00:00.000Z",
isAllDay: false,
},
{
TaskID: 7,
OwnerID: 3,
Title: "Meeting with Alex",
Start: "2023-06-22T14:00:00.000Z",
End: "2023-06-22T16:00:00.000Z",
isAllDay: false,
},
{
TaskID: 9,
OwnerID: 2,
Title: "Alex's Birthday",
Start: "2023-06-18T13:00:00.000Z",
End: "2023-06-18T14:00:00.000Z",
isAllDay: true,
},
];
const currentYear = new Date().getFullYear();
const parseAdjust = (eventDate) => {
const date = new Date(eventDate);
date.setFullYear(currentYear);
return date;
};
// displayDate and sampleData to be used in Scheduler component
export const displayDate = new Date(Date.UTC(currentYear, 5, 24));
export const sampleData = baseData.map((dataItem) => ({
id: dataItem.TaskID,
start: parseAdjust(dataItem.Start),
end: parseAdjust(dataItem.End),
isAllDay: dataItem.isAllDay,
title: dataItem.Title,
ownerID: dataItem.OwnerID,
}));
In our parent App
component instance, we can now import the sampleData
and displayDate
values and use them as props as we render the main Scheduler
component.
import * as React from "react";
import { Scheduler } from "@progress/kendo-react-scheduler";
import { sampleData, displayDate } from "./events-utc";
const App = () => {
return (
<Scheduler data={sampleData} defaultDate={displayDate}>
{/* ... */}
</Scheduler>
);
};
export default App;
Just with the above changes alone, we’ve rendered a simple daily scheduler that lists the events and tasks at their respective days and times!
The KendoReact Scheduler component offers several views to cater to different scheduling needs. Each view can be easily integrated and customized according to our application’s requirements.
For example, we can use the Agenda view to display a weekly summary in a table format.
import * as React from "react";
import { Scheduler, AgendaView } from "@progress/kendo-react-scheduler";
import { sampleData, displayDate } from "./events-utc";
const App = () => {
return (
<Scheduler data={sampleData} defaultDate={displayDate}>
<AgendaView
title="Compact View"
step={2}
numberOfDays={8}
selectedDateFormat={"From: {0:D} To: {1:D}"}
selectedShortDateFormat={"From: {0:d} To: {1:d}"}
/>
</Scheduler>
);
};
export default App;
Alternatively, we can use the Day view to display events in a familiar calendar day-to-day layout.
import * as React from "react";
import { Scheduler, DayView } from "@progress/kendo-react-scheduler";
import { sampleData, displayDate } from "./events-utc";
const App = () => {
return (
<Scheduler data={sampleData} defaultDate={displayDate}>
<DayView
title="Three-Day-View"
numberOfDays={3}
slotDuration={60}
slotDivisions={2}
startTime={"07:00"}
endTime={"19:00"}
workDayStart={"08:00"}
workDayEnd={"18:00"}
/>
</Scheduler>
);
};
export default App;
We can use the Month view to display a high-level schedule overview organized by weeks.
import * as React from "react";
import { Scheduler, MonthView } from "@progress/kendo-react-scheduler";
import { sampleData, displayDate } from "./events-utc";
const App = () => {
return (
<Scheduler data={sampleData} defaultDate={displayDate}>
<MonthView
title="Month"
selectedDateFormat="{0:M}"
selectedShortDateFormat="{0:M}"
/>
</Scheduler>
);
};
export default App;
The Week and WorkWeek views help display a familiar calendar layout with a pre-defined number of days and navigation step.
import * as React from "react";
import {
Scheduler,
WeekView,
WorkWeekView,
} from "@progress/kendo-react-scheduler";
import { Day } from "@progress/kendo-date-math";
import { sampleData, displayDate } from "./events-utc";
const App = () => {
return (
<Scheduler data={sampleData} defaultDate={displayDate}>
<WorkWeekView
title="Work Week"
workWeekStart={Day.Monday}
workWeekEnd={Day.Friday}
/>
<WeekView
title="Full Week"
workWeekStart={Day.Monday}
workWeekEnd={Day.Friday}
/>
</Scheduler>
);
};
export default App;
Lastly, the Timeline view displays events on a continuous time-scale.
import * as React from "react";
import { Scheduler, TimelineView } from "@progress/kendo-react-scheduler";
import { Day } from "@progress/kendo-date-math";
import { sampleData, displayDate } from "./events-utc";
const App = () => {
return (
<Scheduler data={sampleData} defaultDate={displayDate}>
<TimelineView
title="Hour-By-Hour"
numberOfDays={2}
columnWidth={100}
slotDuration={60}
slotDivisions={1}
startTime={"08:00"}
endTime={"18:00"}
workDayStart={"09:00"}
workDayEnd={"17:00"}
workWeekStart={Day.Sunday}
workWeekEnd={Day.Monday}
showWorkHours={false}
/>
</Scheduler>
);
};
export default App;
The KendoReact Scheduler’s flexibility in handling data changes is a critical feature, especially in dynamic scheduling applications where events can be frequently added, updated or deleted. The onDataChange()
callback is central to managing these changes, ensuring that the Scheduler’s state remains in sync with the underlying data.
We’ll expand on the Week view example we created above to first ensure data used in the Scheduler is kept within component state.
import * as React from "react";
import { Scheduler, WeekView } from "@progress/kendo-react-scheduler";
import { Day } from "@progress/kendo-date-math";
import { sampleData, displayDate } from "./events-utc";
const App = () => {
const [data, setData] = React.useState(sampleData);
return (
<Scheduler data={data} defaultDate={displayDate}>
<WeekView
title="Full Week"
workWeekStart={Day.Monday}
workWeekEnd={Day.Friday}
/>
</Scheduler>
);
};
export default App;
The Scheduler component can receive an editable
prop that dictates if the Scheduler component is editable and what types of editing it supports. For our first example, we can enable the drag functionality (i.e., functionality to drag events within the schedule) by setting the editable.drag
property of the Scheduler to true
.
<Scheduler
data={data}
defaultDate={displayDate}
editable={{
drag: true, // Enables dragging of events
}}
>
{/* ... */}
</Scheduler>
When an event is dragged to a new time slot, the onDataChange()
callback of the component is triggered. The onDataChange()
callback can handle various types of data alterations. This callback function encompasses three key collections of items:
For the dragging capability, the updated event will be passed in the updated
array of the callback argument. We’ll use this to update the event state used in our scheduler.
const App = () => {
const [data, setData] = React.useState(sampleData);
const handleDataChange = React.useCallback(
({ updated }) => {
setData((old) =>
old.map(
(item) => updated.find((current) => current.id === item.id) || item
)
);
},
[setData]
);
return (
<Scheduler
data={data}
defaultDate={displayDate}
onDataChange={handleDataChange}
editable={{
drag: true,
}}
>
<WeekView
title="Full Week"
workWeekStart={Day.Monday}
workWeekEnd={Day.Friday}
/>
</Scheduler>
);
};
export default App;
With this change, we’ll be able to modify the times and days an event resides in by simply dragging it across the scheduler!
To delete events from the scheduler, we can enable the editable.remove
property to true
and use the deleted
array of the onDataChange()
callback to update the event state whenever an event is removed.
const App = () => {
const [data, setData] = React.useState(sampleData);
const handleDataChange = React.useCallback(
({ deleted }) => {
setData((old) =>
old.filter(
(item) =>
deleted.find((current) => current.id === item.id) === undefined
)
);
},
[setData]
);
return (
<Scheduler
data={data}
defaultDate={displayDate}
onDataChange={handleDataChange}
editable={{
remove: true,
}}
>
<WeekView
title="Full Week"
workWeekStart={Day.Monday}
workWeekEnd={Day.Friday}
/>
</Scheduler>
);
};
With this change applied, our scheduler provides the capability for us to remove events by clicking a remove icon in the event and confirming the deletion in a confirmation modal that appears.
In addition to being able to drag and delete events, the KendoReact Scheduler also provides the functionality to add and edit events seamlessly. This is achieved by enabling the editable.add
and editable.edit
properties in the Scheduler component and leveraging the created
and updated
arrays in the onDataChange()
callback.
const App = () => {
const [data, setData] = React.useState(sampleData);
const handleDataChange = React.useCallback(
({ created, updated }) => {
setData((old) =>
old
.map(
(item) => updated.find((current) => current.id === item.id) || item
)
.concat(
created.map((item) =>
Object.assign({}, item, {
id: guid(),
})
)
)
);
},
[setData]
);
return (
<Scheduler
data={data}
defaultDate={displayDate}
onDataChange={handleDataChange}
editable={{
add: true,
edit: true,
}}
>
<WeekView
title="Full Week"
workWeekStart={Day.Monday}
workWeekEnd={Day.Friday}
/>
</Scheduler>
);
};
With these additions, our scheduler now fully supports adding and editing events. To add an event, we can double-click a calendar and fill in the details in the add event modal that appears.
To edit an event, we can double-click an existing event in the calendar and modify the event information within the edit modal that appears.
The KendoReact Scheduler component is a versatile tool that can be easily adapted to the dynamic needs of scheduling applications. Through its editable capabilities, developers can provide users with an intuitive and efficient interface for managing their schedules, whether it’s adding, updating, deleting or dragging events.
Everything we’ve covered in this article is only a subset of capabilities and features the KendoReact Scheduler component provides. For more details on advanced features like customization, time zones, globalization, keyboard navigation, etc., be sure to check out the official component documentation!
And don’t forget, KendoReact comes with a free 30-day trial if you’re ready to give it a spin.
]]>In the first part of this series, we covered how to use React Router Loaders to implement data fetching and Actions to handle submitting form data and sending it via an API request. Now we will implement edit and delete functionality and explore how to add pending state UI feedback so users know the form is being processed.
Let’s start by adding a new route in the main.tsx
file.
src/main.tsx
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App.tsx";
import "./index.css";
import { createBrowserRouter, RouterProvider } from "react-router-dom";
import Users from "./views/user/Users.tsx";
import { usersLoader } from "./views/user/Users.loader.ts";
import EditUser from "./views/user/EditUser.tsx";
import { editUserLoader } from "./views/user/EditUser.loader.ts";
import { editUserAction } from "./views/user/EditUser.action.ts";
import CreateUser from "./views/user/CreateUser.tsx";
import { createUserAction } from "./views/user/CreateUser.action.ts";
const router = createBrowserRouter([
{
path: "/",
element: <App />,
children: [
{
index: true,
element: <Users />,
loader: usersLoader,
},
{
path: "/user/create",
element: <CreateUser />,
action: createUserAction,
},
{
path: "/user/:id",
element: <EditUser />,
loader: editUserLoader,
action: editUserAction,
},
],
},
]);
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
<React.StrictMode>
<RouterProvider router={router} />
</React.StrictMode>
);
The EditComponent
will be loaded when the URL matches /user/:id
path. The :id
param is dynamic, so it will match anything with an exception of create
, which is already used for the add user route. In contrast to other routes, the new edit route has defined both a loader and an action because we need to fetch the matching user’s data, and we need an action to submit the data. Let’s add them next.
src/views/user/EditUser.loader.ts
import { LoaderFunctionArgs, redirect } from "react-router-dom";
import { z } from "zod";
import { userSchema } from "../../schema/user.schema";
export const editUserLoader = async ({ params }: LoaderFunctionArgs) => {
try {
const response = await fetch(`http://localhost:4000/users/${params.id}`);
const user = await response.json();
return {
user: userSchema.parse(user),
};
} catch (error) {
return redirect("/");
}
};
export type EditUserLoaderResponse = Exclude<
Awaited<ReturnType<typeof editUserLoader>>,
Response
>;
In the editUserLoader
, we take the id
parameter and use it to fetch details about the user. When we have the response, we validate the received user data. If there are any issues, the user is redirected to the users page. Note how we exclude the Response
interface from the EditUserLoaderResponse
type. The reason for it is that we want to use EditUserLoaderResponse
to assert the type of data returned by the loader inside of a component. However, while the object returned in the try
block contains the user
property, the redirect
method returns the Response
type. Hence, the awaited return type of the editUserLoader
is something like this:
type EditUserLoaderResponse = {
user: { id: string | number; firstName: string; lastName: string; }
} | Response
The image below shows the error that would happen with the above type.
We can exclude the Response
type because we know that if the loader returns a redirect
, then the matching route component for the current URL will not be rendered at all. Therefore, it’s safe to assume that what is returned by the useLoaderData
hook will be the object with the user
property. Let’s add the route action next.
src/views/user/EditUser.action.ts
import { ActionFunctionArgs, redirect } from "react-router-dom";
export const editUserAction = async ({ request }: ActionFunctionArgs) => {
const formData = await request.formData();
const payload = Object.fromEntries(formData.entries());
await fetch(`http://localhost:4000/users/${payload.id}`, {
method: "PATCH",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
});
return redirect("/");
};
The editUserAction
is very similar to the createUserAction
function. The only difference is that we need to use user’s ID in the URL. When the request is successful, the user is redirected back to the users page.
The loaders and actions for editing a user are ready, so it’s time to create the EditUser
component.
src/views/user/EditUser.tsx
import { useLoaderData } from "react-router-dom";
import { EditUserLoaderResponse } from "./EditUser.loader";
import UserForm from "./components/UserForm";
const EditUser = () => {
const { user } = useLoaderData() as EditUserLoaderResponse;
return (
<div className="max-w-sm mx-auto">
<UserForm user={user} action={`/user/${user.id}`} />
</div>
);
};
export default EditUser;
We get the fetched user
data from the loader and pass it to the UserForm
component. Besides that, we also render the UserForm
component and pass user
and action
props. We don’t need to create the UserForm
component, as we did it already in the previous part of this series, but here is the code as a reminder.
src/views/user/components/UserForm.tsx
import { Form } from "react-router-dom";
type UserFormProps = {
className?: string;
user?: {
id: string | number;
firstName: string;
lastName: string;
} | null;
action: string;
};
const UserForm = (props: UserFormProps) => {
const { className, user, action } = props;
return (
<div className={className}>
<Form className="space-y-4" method="post" action={action}>
<input type="hidden" name="id" defaultValue={user?.id} />
<div className="flex flex-col items-start gap-y-2">
<label>First Name</label>
<input
type="text"
className="w-full px-4 py-2 border border-gray-300 rounded-md shadow-sm"
name="firstName"
defaultValue={user?.firstName || ""}
/>
</div>
<div className="flex flex-col items-start gap-y-2">
<label>Last Name</label>
<input
type="text"
className="w-full px-4 py-2 border border-gray-300 rounded-md shadow-sm"
name="lastName"
defaultValue={user?.lastName || ""}
/>
</div>
<div>
<button
type="submit"
className="w-full px-4 py-3 mt-4 font-semibold bg-sky-600 text-sky-50"
>
Save
</button>
</div>
</Form>
</div>
);
};
export default UserForm;
That’s all the code we need for the edit functionality. We can click on one of the users, update the name and surname and click the Save
button to update the user. The GIF below shows what it looks like.
We can create, edit and display users, but we can’t delete them yet, so let’s add that functionality.
We will add a delete button in the UserForm
component, so we can update or delete a user. However, before we do that, we need an answer to an important question: How can we have multiple actions per route? After all, we need one action for updating a user and another one for deletion. The thing is, we can’t. We can have only one action.
However, inside an action, we can figure out what should be done based on the payload. Hence, we will add a delete button and update the current save button with two attributes: name
and value
. Those will be used to execute either update or delete flow.
src/views/user/components/UserForm.tsx
import { Form } from "react-router-dom";
type UserFormProps = {
className?: string;
user?: {
id: string | number;
firstName: string;
lastName: string;
} | null;
action: string;
};
const UserForm = (props: UserFormProps) => {
const { className, user, action } = props;
return (
<div className={className}>
<Form className="space-y-4" method="post" action={action}>
<input type="hidden" name="id" defaultValue={user?.id} />
<div className="flex flex-col items-start gap-y-2">
<label>First Name</label>
<input
type="text"
className="w-full px-4 py-2 border border-gray-300 rounded-md shadow-sm"
name="firstName"
defaultValue={user?.firstName || ""}
/>
</div>
<div className="flex flex-col items-start gap-y-2">
<label>Last Name</label>
<input
type="text"
className="w-full px-4 py-2 border border-gray-300 rounded-md shadow-sm"
name="lastName"
defaultValue={user?.lastName || ""}
/>
</div>
<div className="space-y-4">
<button
type="submit"
name="intent"
value="save
className="w-full px-4 py-3 mt-4 font-semibold bg-sky-600 text-sky-50"
>
Save
</button>
{user ? (
<button
type="submit"
name="intent"
value="delete"
className="w-full px-4 py-3 font-semibold bg-gray-100 hover:bg-gray-200"
>
Delete
</button>
) : null}
</div>
</Form>
</div>
);
};
export default UserForm;
Next, we need to update the editUserAction
method.
src/views/user/EditUser.action.ts
import { ActionFunctionArgs, redirect } from "react-router-dom";
import { User, userSchema } from "../../schema/user.schema";
const deleteUser = async (userId: string | number) => {
return fetch(`http://localhost:4000/users/${userId}`, {
method: "delete",
});
};
const editUser = async (payload: User) => {
return fetch(`http://localhost:4000/users/${payload.id}`, {
method: "PATCH",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
});
};
export const editUserAction = async (args: ActionFunctionArgs) => {
const { request } = args;
const formData = await request.formData();
const { intent, ...payload } = Object.fromEntries(formData.entries());
const userData = userSchema.parse(payload);
if (intent === "delete") {
await deleteUser(userData.id);
}
if (intent === "save") {
await editUser(userData);
}
return redirect("/");
};
Inside the editUserAction
function, the intent
value is separated from the rest of the form data. If its value is delete
, the deleteUser
function is called and if it’s save
, then editUser
is executed instead. That’s how we can handle multiple behaviors in one action.
Sometimes it can take a while for API requests to be processed. Therefore, to improve user experience, we can show feedback that something is happening in response to user’s interaction.
For instance, if a user clicks on the save or delete buttons, we could change the text or show a spinner. To keep things simple, we will just change texts from Save
to Saving...
and Delete
to Deleting...
. The buttons will also be disabled when the action is pending.
React Router 6 has a hook called useNavigation
that provides information about page navigation. It can be used to obtain information, such as whether there is pending navigation and more. You can read more about it in detail here. We will use this hook in the UserForm
component to disable form buttons and change their text.
src/views/user/components/UserForm.tsx
import { Form, useNavigation } from "react-router-dom";
type UserFormProps = {
className?: string;
user?: {
id: string | number;
firstName: string;
lastName: string;
} | null;
action: string;
};
const UserForm = (props: UserFormProps) => {
const { className, user, action } = props;
const navigation = useNavigation();
const isSubmitPending =
navigation.state === "submitting" && navigation.formMethod === "post";
const isDeletePending =
navigation.state === "submitting" && navigation.formMethod === "delete";
return (
<div className={className}>
<Form className="space-y-4" method="post" action={action}>
<input type="hidden" name="id" defaultValue={user?.id} />
<div className="flex flex-col items-start gap-y-2">
<label>First Name</label>
<input
type="text"
className="w-full px-4 py-2 border border-gray-300 rounded-md shadow-sm"
name="firstName"
defaultValue={user?.firstName || ""}
/>
</div>
<div className="flex flex-col items-start gap-y-2">
<label>Last Name</label>
<input
type="text"
className="w-full px-4 py-2 border border-gray-300 rounded-md shadow-sm"
name="lastName"
defaultValue={user?.lastName || ""}
/>
</div>
<div className="space-y-4">
<button
type="submit"
name="intent"
value="save"
className="w-full px-4 py-3 mt-4 font-semibold bg-sky-600 text-sky-50"
disabled={isSubmitPending || isDeletePending}
>
{isSubmitPending ? "Saving..." : "Save"}
</button>
{user ? (
<button
type="submit"
name="intent"
value="delete"
formMethod="delete"
className="w-full px-4 py-3 font-semibold bg-gray-100 hover:bg-gray-200"
disabled={isSubmitPending || isDeletePending}
>
{isDeletePending ? "Deleting..." : "Delete"}
</button>
) : null}
</div>
</Form>
</div>
);
};
export default UserForm;
We use navigation.state
and navigation.formMethod
to get isSubmitPending
and isDeletePending
values.
const navigation = useNavigation();
const isSubmitPending =
navigation.state === "submitting" && navigation.formMethod === "post";
const isDeletePending =
navigation.state === "submitting" && navigation.formMethod === "delete";
Those are then used in the save and delete buttons.
<button
type="submit"
name="intent"
value="save"
formMethod="post"
className="w-full px-4 py-3 mt-4 font-semibold bg-sky-600 text-sky-50"
disabled={isSubmitPending || isDeletePending}
>
{isSubmitPending ? "Saving..." : "Save"}
</button>
{user ? (
<button
type="submit"
name="intent"
value="delete"
formMethod="delete"
className="w-full px-4 py-3 font-semibold bg-gray-100 hover:bg-gray-200"
disabled={isSubmitPending || isDeletePending}
>
{isDeletePending ? "Deleting..." : "Delete"}
</button>
) : null}
Note that the Delete
button also has a new attribute called formMethod
. When the form is submitted using the Delete
button, its form method is changed from post
to delete
. This attribute is needed so we can distinguish between save and delete buttons and show a different text for the button that was clicked. Before we finish, let’s add a bit of an artificial delay to the editUserAction
, so we can see the text update.
src/views/user/EditUser.action.ts
export const editUserAction = async (args: ActionFunctionArgs) => {
await new Promise(resolve => setTimeout(resolve, 1000));
/**
...other code...
**/
return redirect("/");
};
Here’s what it looks like in action.
Loaders and Actions are powerful features that solve common data fetching and submission challenges in React applications. They are great tools that can enhance overall functionality and user experience.
By utilizing Loaders, we overcome the waterfall issue caused by in-component data fetching and de-couple components from data fetching.
Actions, on the other hand, offer a simple approach to handling form submissions and performing needed actions before navigating.
We only covered a part of the new features introduced in React Router 6, so make sure to check out the documentation to find out more.
]]>React Router 6 introduced a number of new features in version 6.4. The two especially compelling features are Loaders and Actions that streamline data fetching and form submission in React applications. In this series, we will cover how to use Loaders to fetch a list of users and a specific user to populate an edit user form as well as Actions to create, edit and delete users. Besides that, we will dive into new React Router hooks and use them to show UI feedback during form submission.
To follow along with this article, run the commands below.
git clone git@github.com:ThomasFindlay/demystifying-loaders-and-actions-in-react-router-6.git
cd demystifying-loaders-and-actions-in-react-router-6
git checkout start
npm install
npm run dev
These commands will clone the GitHub repository for this project, switch the branch, install libraries and start the dev server. The project has a few libraries already installed. Besides the react-router-dom
library (React Router), we also have Zod for validation, Tailwind CSS for styling and json-server, which will serve as a CRUD server.
You can also find an interactive example in the StackBlitz below.
Let’s start by adding the router to the project. We will do it in the main.tsx
file, which is the entry point for the application.
main.tsx
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App.tsx";
import "./index.css";
import { createBrowserRouter, RouterProvider } from "react-router-dom";
import Users from "./views/user/Users.tsx";
import { usersLoader } from "./views/user/Users.loader.ts";
const router = createBrowserRouter([
{
path: "/",
element: <App />,
children: [
{
index: true,
element: <Users />,
loader: usersLoader,
},
],
},
]);
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
<React.StrictMode>
<RouterProvider router={router} />
</React.StrictMode>
);
In the router
, we specified that the App
component will be rendered when the URL matches the /
path. Currently, it has a one child route, which also will be rendered when the path is /
, as it has the index
property set to true
. There will be more routes later on for creating and editing users.
Did you see the loader: usersLoader
part in the route definition? That’s how we configure a loader for a specific route. Before React Router resolves and shows the <Users />
element, it will first execute the usersLoader
loader and wait for it to finish. A big advantage of loaders is that they decouple data fetching from the component rendering, thus avoiding the waterfall problem.
The waterfall problem refers to the issue that arises when data is fetched synchronously in a sequential manner, causing delays and blocking the execution of subsequent tasks. It can become noticeable when fetching data in a component tree. For instance, if you have a parent component that needs to fetch data before rendering its child components. In such a scenario, the rendering of the entire component tree is blocked until the data is fetched, leading to slower rendering and a less responsive user interface. And that’s where the loaders shine, as the data fetching is done on the route level rather than the component level.
Now, let’s update the App
component to render an Outlet
, which is responsible for showing a route component that matches the current URL and the Suspense
component to display a loader when data is being fetched.
App.tsx
import { Suspense } from "react";
import "./App.css";
import { Outlet } from "react-router-dom";
function App() {
return (
<div>
<Suspense fallback={<div>loading...</div>}>
<Outlet />
</Suspense>
</div>
);
}
export default App;
In the usersLoader
we will fetch a list of users. But before we get to it, let’s create a Zod schema for a user object.
src/schema/user.schema.ts
import { z } from "zod";
export const userSchema = z.object({
id: z.union([z.string(), z.number()]),
firstName: z.string(),
lastName: z.string(),
});
export type User = z.infer<typeof userSchema>;
As you can see, a user object will consist of three properties—id
, firstName
and lastName
. We will use Zod to validate the fetched data and to narrow down the response type.
Next, let’s create the usersLoader
method.
src/views/user/Users.loader.ts
import { LoaderFunctionArgs } from "react-router-dom";
import { z } from "zod";
import { userSchema } from "../../schema/user.schema";
export const usersLoader = async ({ params }: LoaderFunctionArgs) => {
const response = await fetch("http://localhost:4000/users");
const users = await response.json();
return {
users: z.array(userSchema).parse(users),
};
};
export type UsersLoaderResponse = Awaited<ReturnType<typeof usersLoader>>;
The first thing that happens in the usersLoader
is an API request to fetch users data. Further, the userSchema
is used to validate the response data. Lastly, an object is returned from the loader.
Note that the API endpoint we target here is the one provided by json-server
. The json-server
is running on port 4000. You can find and modify the data served by json-server
in the server/db.json
file. Besides the usersLoader
function, we also have the UsersLoaderResponse
type. Its type is inherited from the return type of the loader.
It’s time to create the Users
component.
src/views/user/Users.tsx
import { useLoaderData, Link } from "react-router-dom";
import { UsersLoaderResponse } from "./Users.loader";
const Users = () => {
const { users } = useLoaderData() as UsersLoaderResponse;
return (
<div className="max-w-sm mx-auto">
<h1 className="text-semibold text-2xl mb-6">Users</h1>
<ul className="space-y-2">
{users.map(user => {
return (
<li key={user.id}>
<Link to={`/user/${user.id}`} className="hover:underline">
{user.firstName} {user.lastName}
</Link>
</li>
);
})}
</ul>
<Link
to="/user/create"
className="inline-block bg-sky-600 text-sky-50 px-4 py-3 font-semibold w-full mt-4"
>
Add User
</Link>
</div>
);
};
export default Users;
React Router provides the useLoaderData
hook to get access to the data returned from the loader assigned for the route. The return type of the useLoaderData
is unknown
by default, so we assert the returned value type to UsersLoaderResponse
. It is derived from the usersLoader
function in the Users.loader.tsx
file.
export type UsersLoaderResponse = Awaited<ReturnType<typeof usersLoader>>;
Furthermore, the Users
component uses the array of users
fetched by the loader to render a list of users with their names and surnames. Each user item is wrapped with a link which redirects to the edit user form. Moreover, there is also a link to the add user form. The image below shows what the final output looks like.
Next, let’s create functionality to add new users.
So far, we have used a loader to fetch the users data. Now, we will use an action to submit a user’s data. We will start by adding a new route in the main.tsx
file.
src/main.tsx
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App.tsx";
import "./index.css";
import { createBrowserRouter, RouterProvider } from "react-router-dom";
import Users from "./views/user/Users.tsx";
import { usersLoader } from "./views/user/Users.loader.tsx";
import CreateUser from "./views/user/CreateUser.tsx";
import { createUserAction } from "./views/user/CreateUser.action.tsx";
const router = createBrowserRouter([
{
path: "/",
element: <App />,
children: [
{
index: true,
element: <Users />,
loader: usersLoader,
},
{
path: "/user/create",
element: <CreateUser />,
action: createUserAction,
},
],
},
]);
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
<React.StrictMode>
<RouterProvider router={router} />
</React.StrictMode>
);
The CreateUser
component will be rendered when the URL matches the /user/create
path. Instead of a loader
property, we specify an action
with the createUserAction
function as a value. Let’s create it now.
src/view/user/CreateUser.action.ts
import { ActionFunctionArgs, redirect } from "react-router-dom";
export const createUserAction = async ({ request }: ActionFunctionArgs) => {
// Get the form data from the request
const formData = await request.formData();
// Convert the form data to an object format
const payload = Object.fromEntries(formData.entries());
await fetch("http://localhost:4000/users", {
method: "post",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
...payload,
id: Date.now(),
}),
});
return redirect("/");
};
React Router expects data to be passed to an action using FormData. However, as the server expects a JSON payload, we convert it from FormData to JSON. After the create user request is completed, a user will be redirected to the /
path.
Next, let’s create the CreateUser
component.
src/views/user/CreateUser.tsx
import UserForm from "./components/UserForm";
const CreateUser = () => {
return (
<div className="max-w-sm mx-auto">
<UserForm action="/user/create" />
</div>
);
};
export default CreateUser;
The CreateUser
component is only responsible for the layout and rendering of the UserForm
component. The UserForm
component receives the action
prop, which needs to match the route path defined for the current route. React Router will use it to find the matching route action. Let’s create the UserForm
component next. We put the form in a separate component, as we will re-use it for both creating and editing users.
src/views/user/components/UserForm.tsx
import { Form } from "react-router-dom";
type UserFormProps = {
className?: string;
user?: {
id: string | number;
firstName: string;
lastName: string;
} | null;
action: string;
};
const UserForm = (props: UserFormProps) => {
const { className, user, action } = props;
return (
<div className={className}>
<Form className="space-y-4" method="post" action={action}>
<input type="hidden" name="id" defaultValue={user?.id} />
<div className="flex flex-col items-start gap-y-2">
<label>First Name</label>
<input
type="text"
className="w-full px-4 py-2 border border-gray-300 rounded-md shadow-sm"
name="firstName"
defaultValue={user?.firstName || ""}
/>
</div>
<div className="flex flex-col items-start gap-y-2">
<label>Last Name</label>
<input
type="text"
className="w-full px-4 py-2 border border-gray-300 rounded-md shadow-sm"
name="lastName"
defaultValue={user?.lastName || ""}
/>
</div>
<div>
<button
type="submit"
className="w-full px-4 py-3 mt-4 font-semibold bg-sky-600 text-sky-50"
>
Save
</button>
</div>
</Form>
</div>
);
};
export default UserForm;
The UserForm
component accepts three props: className
, user
and action
. The user
prop will be used to prefill the form with the user’s data when editing. However, it’s optional, as there is no user data yet when we create one. The action
prop is passed to the Form
component and specifies what action the form is for. When using React Router, the action should match the route path of the action handler the form should trigger.
The UserForm
component renders three inputs, of which only two are visible to the user. One is hidden, and it’s used to store the user’s id. If you’re wondering why there is no useState
anywhere to store the form’s state, it’s because, in this example, we are using uncontrolled components. In a nutshell, the form’s state is controlled by the DOM rather than by React. We just provide a default value if there is any to the inputs.
Here’s what the functionality looks like.
After submitting the form, we are redirected to the users page, where we can see that a new user was added successfully.
In this part, we have learned how to use React Router Loaders to fetch a list of users and Actions to create a new user. In the next part, we will implement functionality to edit and delete users, how to handle multiple actions in the same route and add pending state feedback for an appropriate submit action when the form is being processed.
]]>