Overview

These are examples to study and adapt when doing tutorials and projects in React. These samples assume familiarity with modern JavaScript and a basic knowledge of the structure of a React application.

For more on modern JavaScript programming, see this introduction and these notes. For basic React concepts, see this short list of basic concepts.

For quick reference, the steps or code to do come first, followed by notes explaining how the steps or code work, alternatives, warnings, and so on.

There are several reasons why these examples may differ from other code you find online:

  • React and JavaScript continues to evolve. Example code here tries to be as current as possible, using hooks and functional components, not classes.
  • Example code tries to be as simple as possible, to support learning and prototyping. TypeScript, Redux, and other frameworks useful for large-scale applications are not introduced.

When you see significant differences or contradictions, ASK!

Installing Node

Test for the presence of Node. Open a terminal window. On MacOS use the Terminal. On Windows, run Git Bash which is included with Git for Windows. Then do

node --version
nvm --version

You need at least Node 14 to run React and Firebase. Node 18 is the current version and is recommended.

If you have no Node, jump to the steps for installing nvm (Node Version Manager). If you have an older Node and no nvm, uninstall Node manually and then install nvm. If you have an older Node and nvm, jump to the steps for installing a current version of Node.

Uninstall older Node manually

To uninstall an older Node without nvm:

Install nvm

Install nvm if not present.

Install a current version of Node

With nvm you install one or more versions of Node, then separately tell nvm which Node to use.

nvm install node
nvm use node
On Windows, to run nvm use, you must either enable Developer mode (Windows 10), or run Git Bash in Adminstrator mode (Windows 8).

Verify the installation

Open a new terminal window and run:

node --version

Verify you see the version of Node 18 you installed.

Notes

Node (also known as NodeJS) is a scripting environment, like Python or Ruby, but based on the JavaScript language. It is used by React to build a web app from source code and run a local server for testing.

Installing Node also installs npm and npx. npm is the Node Package Manager. It is used to install Node libraries in applications. It downloads them into the directory node_modules in your application code. It keeps track of the libraries your application needs in the file package.json.

npx stands for Node Package Execute. It downloads and runs Node scripts, such as degit and create-react-app.

nvm is the Node Version Manager. It lets you install multiple versions of Node and switch between them. nvm avoids permission issues that often occur with installing Node manually. With nvm you should never need to use sudo to install libraries on MacOS or Linux.

Initializing a React app

Vite is an application for creating new React apps. Vite is much faster and uses much less disk space than the older create-react-app. No one should use that any more.

Start a React project with Vite

Follow these instructions. react-vitest is a template for React apps that includes Vitest and the react-testing-library.

Some notes about React applications built with Vite:

  • Component files that include JSX, as most do, must have the file extension .jsx not .js.
  • The entry point for the application, index.html, is in the root directory. You rarely touch this file.
  • The command npm run build puts the assembled application in the subdirectory dist.
  • The testing framework is Vitest, a clone of Jest, built to work with Vite.

Creating a Github repo for existing code

Once you have created a React app, you should create a Github repository for it.

Stop your app in the command shell with control-C, if necessary.

Go to Github and follow these instructions for creating a remote repository to hold existing code. Do not create any default files such as README or .gitignore. Those have already been created.

Hosting an app on a public server

Use Firebase to host your web page on a public server for testing and demoing.

Create a Firebase project to host your React app

Follow these instructions to create a project to host your React app.

Northwestern Google accounts do not support Firebase. To create a Firebase project, you need to use a personal Google account, or create one if necessary.

Note that some steps are done are on the Firebase web console, and others are done on your local machine. When setting things up on your local machine,

  • Use npm -g to install firebase-tools globally.
  • When Firebase asks for the public directory, do not accept the default.
    • Specify dist for a Vite project.
  • Say no when Firebase asks about adding Github Actions.

Building and deploying to Firebase

To deploy your app:

npm run build
firebase deploy

The build step creates a "production" version of your app. This is very different than the version created when you do npm run start. Code is optimized, compacted, and placed into a few files.

Notes

Firebase can be used to host simple web pages, including React apps. Using Firebase to host makes even more sense if you are or will be using other Firebase services, such as the database or authentication service. Other hosting options include Github Pages, AWS, and Heroku.

You create a project on the Firebase site to organize access to one or more Firebase services, including hosting, data storage, and authentication. Multiple apps -- both web-based and native -- can access the same project if they share the same backend data.

Changing an app title or favicon

To change the title or favicon for a web app that appear in a browser tab and history, edit the lines marked below in the index.html of your app.

<!DOCTYPE html>
  <html lang="en">
    <head>
      <meta charset="UTF-8" />
      <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
      <meta name="viewport" content="width=device-width, initial-scale=1.0" />
      <title>React App</title>
    </head>
    <body>
      <div id="root"></div>
      <script type="module" src="/src/index.jsx"></script>
    </body>
  </html>

Notes

You can use an online tool to create a favicon. Free Favicon Maker is nice because you can make a favicon from an emoji without uploading an image. It can create SVG icons which are smaller than PNG files. If you create a PNG file instead, you should adjust the type attribute of the link .

This is the only task where you will edit an HTML file. Normally, you will be editing JavaScript code that creates HTML. The title and/or favicon are different because they are specified in the HTML head rather than HTML body.

If you created your app with create-react-app, the file index.html will be in the public subdirectory, and have additional code, but the same parts need to be changed.

Defining App

To define an app, replace the default JavaScript code created by Vite or create-react-app in App.jsx with code that defines the App functional component. For example, the following defines an app that shows the current date.

const App = () => {
  const today = new Date();
  const day = today.toLocaleString([], {weekday: 'long'});
  const date = today.toLocaleDateString([], {dateStyle: 'long'})

  return (
    <div>
      <h1>Sample React code</h1>
      
      <p>Today is {day}, {date}.</p>
    </div>
  );
};

export default App;

Notes

With React, you don't edit HTML. You define JavaScript functions, called components, that React calls to generate HTML. By default, React calls the component App, defined in in src/App.jsx, to create an app.

See React starting points for an introduction to key concepts such as component functions, JSX, and React state.

Defining a basic component

Here is a React component that displays the name, email, phone number, and U.S. address of a user.

const UserContact = ({user}) => (
  <table>
    <tbody>
      <tr><th>Name</th><td>{user.firstName} {user.lastName}</td></tr>
      <tr><th>Email</th><td>{user.email}</td></tr>
      <tr><th>Phone</th><td>{user.phone}</td></tr>
      <tr>
        <th>Address</th>
        <td>
          {user.address.address}, {user.address.city}, {user.address.state} {user.address.postalCode}
        </td>
      </tr>
    </tbody>
  </table>
);

export default UserContact;

Related links

Notes

A React component is a function that returns HTML code. Data for UserContact will be passed in a key-value object. The component uses object destructuring to get the value of the user key.

This function is simple enough to be written as a concise arrow function body with no curly braces or return call.

Major components are usually defined as the default export in a separate file. The name of the file should match the name of the component, including capitalization. The extension should be jsx. By convention, those files are put into a subdirectory called components.

Defining a component with multiple children

A component has to return a single object. This can be a div or ul. When you need to return several elements that should not be nested, e.g., several elements of a list or a form, you can make a React fragment, like this:

const LabeledCheckbox = ({id, text}) => (
  <>
    <input class="form-check-input" type="checkbox" value="" id={id}>
    <label class="form-check-label" for={id}>
      {text}
    </label>
  </>
);

export default LabeledCheckbox;

The syntax <>...</> is shorthand for <React.Fragment>...</React.Fragment>.

Creating a list of components

Here is a component that displays the contact information for a list of users.

import UserContact from './UserContact';
          
const UserContacts = ({users}) => (
  <div>
    { Object.entries(users).map(([id, user]) => <UserContact key={id} user={user} />) }
  </div>
);

export default UserContacts;

Related links

Notes

Code in JSX curly braces must be an expression. A for loop would not work because it is not an expression. A map() works because it is a function call, which is an expression.

A component must return one HTML element or React component. Therefore, UserContacts puts the list of UserContact components inside a div.

If a list of components is created, React requires each element to have a unique key attribute that it can use for efficient updates. ContactList uses the user id field as the key.

Read more on JSX syntax and list keys.

Installing Bootstrap

To install the Bootstrap library into a React project

npm install bootstrap

To make the Bootstrap CSS classes accessible, import Bootstrap. Typically this is done in src/App.jsx:

import 'bootstrap/dist/css/bootstrap.min.css';

Bootstrap has an icon library that can be installed similarly.

npm i bootstrap-icons

It also needs to be imported:

import 'bootstrap-icons/font/bootstrap-icons.css';

Styling a component with Bootstrap

Here is a version of the example UserContact table styled with Bootstrap 5.

const UserContact = ({user}) => (
  <table className="table table-bordered">
    <tbody>
      <tr><th style={{width: '7em'}}>Name</th><td>{user.firstName} {user.lastName}</td></tr>
      <tr><th>Email</th><td>{user.email}</td></tr>
      <tr><th>Phone</th><td>{user.phone}</td></tr>
      <tr>
        <th>Address</th>
        <td>
          {user.address.address}, {user.address.city}, {user.address.state} {user.address.postalCode}
        </td>
      </tr>
    </tbody>
  </table>
);

export default UserContact;

Required libraries

Notes

In JSX, you must write className rather than class because class is a reserved word in JavaScript.

style={{width: '7em'}} specifies an inline style with a JavaScript object. Style values must be strings, e.g., '7em', and any CSS property names must be camelcased, e.g, background-color: gray becomes backgroundColor: 'gray'.

CSS styles and classes can only be attached to HTML elements, not React components. The following does not work:

<UserContact className="table" user={user} />

In order to have reasonable whitespace around the entire app, put everything inside a Bootstrap container:

const App = () => (
  <div className="container">
    ...rest of the app...
  </div>
);

You can delete or replace the App.css file created by Vite and create-react-app. There's also an src/index.css loaded by index.html. If you delete that, edit index.html accordingly.

An alternative way to style React apps is to use a library of components with styles included. For two common libraries that do this, see React Bootstrap and MaterialUI.

Defining component-specific CSS

Component-specific CSS files can be imported like this

import './ProductList.css';
...

Default CSS, such as Bootstrap or a reset CSS file should be imported in App.jsx before any components are imported, so that component CSS will override the defaults.

import 'bootstrap/dist/css/bootstrap.min.css';
import 'bootstrap-icons/font/bootstrap-icons.css';
import './App.css';
import ProductList from './components/ProductList';
...

Notes

Overriding styles is sometimes necessary, but in general if components need specific CSS rules, create a CSS class name for the component and use that in the CSS rules.

Another approach to component-specific CSS is to use styled components.

Defining Bootstrap cards

Cards -- rectangular bordered boxes with content -- are a popular format for displaying multiple pieces of similar content. Bootstrap 5 provides a set of CSS classes for creating cards.

Below is the code for the product card on the left.

import './Product.css';

const formatPrice = new Intl.NumberFormat([], { style: 'currency', currency: 'USD' }).format;

const Product = ({product}) => (
  <div className="card m-1 p-2">
    <img src={product.thumbnail} className="card-img-top" alt={product.description} />
    <div className="card-body">
      <h5 className="card-title">{product.title}</h5>
      <p className="card-text">{product.title}</p>
      <p className="card-text">{formatPrice(product.price)}</p>
    </div>
  </div>
);

export default Product;

The following CSS makes the images at the top of each card a constant height.

/* https://stackoverflow.com/a/47698201/4564359 */
.product .card-img-top {
  width: 100%;
  height: 15vw;
  object-fit: cover;
}

Required libraries

Related links

Notes

Bootstrap cards provide a nice structure for defining a bordered container. Use the Bootstrap classes m-1 and p-2 on each card to add some whitespace around and inside a card. See Bootstrap spacing.

Defining a grid of cards

CSS provides a grid class to display items in a regular rectangular array like this:

Here is the code for a grid of product cards.

import Product from './Product';
import './Products.css'

const ProductList = ({products}) => (
  <div className="product-list">
    {
      products.map(product => <Product key={product.id} product={product} />)
    }
  </div>
);

export default ProductList;

Here is the CSS to implement the grid.

.product-list {
  display: grid;
  grid-template-columns: repeat(auto-fill, 14rem);
}

Related links

Notes

This code is using CSS grid. Grid in Bootstrap 5 is not based on the CSS grid. There is experimental support for CSS grids in Bootstrap 5.1 but it is not easy to use yet.

The CSS repeat(auto-fill, ...) value for grid-template-columns provides a compact way to horizontally place as many objects of a certain size as can fit in the current screen width.

More on CSS grid.

Defining React state

Here is a component with React state. It is a button that initially says Rock. If clicked, it changes to Paper. If clicked again, it changes to Scissors. If clicked again, it changes to Rock, and so on.

import { useState } from 'react';

const choices = ['Rock', 'Paper', 'Scissors'];

const Chooser = () => {
  const [choice, setChoice] = useState(0);

  const nextChoice = () => {
    setChoice((choice + 1) % choices.length);
  }

  return (
    <button onClick={nextChoice}>{choices[choice]}</button>
  );
};

export default Chooser;

Notes

State is a central concept in React. You need state to implement almost any interactive element in a React app.

useState() creates an internal state variable choice to hold what the button should say. It also creates a function setChoice() to use to change that state.

Calling setChoice() when the button is clicked changes the internal state. Changing state causes React to re-render the component that created the state, which sets choice to the new value and changes what is on screen.

This is a critical pattern in React. Code does not change HTML directly. Code changes state, and changes to state trigger screen updates.

For more on React state, see the notes on state.

Defining radio buttons

Here is an example of radio buttons in React with Bootstrap. Selecting any button unselects the others. In this example, selecting Breakfast, Lunch, or Dinner will change what menu items would be shown.

import { useState } from "react";

const meals = {
  Breakfast: 'Breakfast items...',
  Lunch: 'Lunch items...',
  Dinner: 'Dinner items...'
};

const MenuButton = ({meal, selection, setSelection}) => (
  <div>
    <input type="radio" id={meal} className="btn-check" checked={meal === selection} autoComplete="off"
      onChange={() => setSelection(meal)} />
    <label className="btn btn-success mb-1 p-2" htmlFor={meal}>
    { meal }
    </label>
  </div>
);

const MenuSelector = ({selection, setSelection}) => (
  <div className="btn-group">
    { 
      Object.keys(meals).map(meal => <MenuButton key={meal} meal={meal} selection={selection} setSelection={setSelection} />)
    }
  </div>
);

const Menu = ({selection}) => (
  <div className="card" >
  { meals[selection] }
  </div>
);

const MenuPage = () => {
  const [selection, setSelection] = useState(() => Object.keys(meals)[0]);
  return (
    <div>
      <MenuSelector selection={selection} setSelection={setSelection} />
      <Menu selection={selection} />
    </div>
  );
}

export default MenuPage;

Notes

The menu button styling is based on the sample code for Bootstrap radio toggle buttons. To avoid separating each button and label with a div, MenuButton wraps them using a React fragment.

MenuButton is a React controlled component. It is controlled by the meal state variable defined in MenuPage. A button is checked if its name matches meal. Clicking a button updates the state, leading to re-rendering.

The state variable meal has to be created in MenuPage not MenuSelector, so that it can be shared between components.

Here is an example of a general modal dialog in React using Bootstrap that could be used to display things like a shopping cart.

import './Modal.css';

// https://codebuckets.com/2021/08/08/bootstrap-modal-dialog-in-react-without-jquery/

const Modal = ({ children, open, close }) => (
  <div
    className={`modal ${open ? 'modal-show' : 'modal'}`}
    tabIndex="-1"
    role="dialog"
    onClick={(evt) => { if (evt.target === evt.currentTarget) close(); }}
  >
    <div className="modal-dialog" role="document">
      <div className="modal-content">
        <div className="modal-header">
          <button type="button" className="btn-close" aria-label="Close"
            onClick={close}
          />
        </div>
        <div className="modal-body">
          {children}
        </div>
      </div>
    </div>
  </div>
);

export default Modal;

The CSS needed for this code is

.modal {
  display: block;
  visibility: hidden;
}

.modal-show {
  visibility:visible;
  background-color: rgba(169, 169, 169, 0.8);
  transition: opacity 0.2s linear; 
} 

.modal-content  {
  opacity: 0; 
}

.modal-show .modal-content  {
  opacity: 1; 
  transition: 0.2s linear; 
}

.modal-header {
  margin-left: auto;
}

Required libraries

Related links

Notes

The Bootstrap CSS is adapted from the Bootstrap Modal component. The integration with React is adapted from Bootstrap Modal Dialog in React without JQuery.

Critical to most implementations of modal dialg boxes is a background part that covers the entire screen. The background prevents clicking on other parts of the interface until the modal is closed.

Modal uses the React children prop so that another component, as a shopping cart, can nest it inside it.

Modal is passed three properties. The modal will show itself if open is true. The modal will call the function close to close itself. The modal will show the children nested inside the Modal inside the modal.

The Modal will call the close function if the user clicks the "X" in the upper right corner or anywhere in the background.

Here is an example of adding a modal dialog box to show a shopping cart.

import { useState } from 'react';
import Modal from './Modal';
import ProductList from './ProductList';

const ProductPage = () => {
  const [selected, setSelected] = useState([]);
  const [open, setOpen] = useState(false);

  const openModal = () => setOpen(true);
  const closeModal = () => setOpen(false);
  ...
  return (
    <div>
      <button className="btn btn-outline-dark" onClick={openModal}><i className="bi bi-cart4"></i></button>
      <Modal open={open} close={closeModal}>
        <Cart selected={selected} />
      </Modal>
      <ProductList products={data.products} selected={selected} toggleSelected={toggleSelected} />
    </div>
  );
};

Here is a bare-bones Cart component:

import './Cart.css';

const formatPrice = new Intl.NumberFormat([], { style: 'currency', currency: 'USD' }).format;

const Cart = ({selected}) => (
  <div className="cart">
    {
      selected.length === 0
      ? <h2>The cart is empty</h2>
      : selected.map(product => (
          <div key={product.id}>
            {product.title} -- {formatPrice(product.price)}
          </div>
        ))
    }
  </div>
);

export default Cart;

Required libraries

Related links

Defining JavaScript code modules

Here's how to define and export functions from a file.

const sq = x => x * x;

const sqSum = (x, y) => sq(x) + sq(y);

export const hypotenuse = (a, b) => Math.sqrt(sqSum(a, b))

export const perimeter = (a, b, c) => a + b + c;

export const area = (a, b, c) => {
  const s = (a + b + c) / 2;
  return Math.sqrt(s * (s - a) * (s - b) * (s - c));
}

Related links

Notes

Pure JavaScript is typically put in a directory for utility code, such as src/utilities, with the extension js. The file name should not be capitalized.

A function is exported by prefacing its definition with export. Functions that are not exported are private to the module.

Importing code, data, and assets

import is used in React to load a wide variety of assets.

Import a component that is a default export. Note that there are no curly braces. Do not include the file extension.

import UserContact from './components/UserContact';

Import functions exported from a file. Do not include the file extension.

import { area, perimeter } from '../utilities/geometry';

Import data from a JSON file into a variable. Do include the file extension.

import users from './data/users.json';

Import, i.e., load and define, CSS. Do include the file extension.

import './UserContact.css';

Load a local image file and store its URL in a variable. Do include the file extension.

import logoUrl from './images/small-log.png';

Import paths

The path to use to import something will depend on the relative locations of data being imported and the code doing the import.

To import from an installed Node library, just give the module name.

import { useQuery } from "@tanstack/react-query";

To import from a file in your application directory, give a relative path. To import something in or below the directory with the code calling import, start the path with period-slash. To import something in a sibling directory, start the path with two periods and a slash.

To import..Into...Use the path...
src/components/UserContact.jsxsrc/App.jsx'./components/UserContact'
src/components/UserContact.jsxsrc/components/ContactList.jsx'./UserContact'
src/utilities/triangle.jsxsrc/components/Triangle.jsx'../utilities/triangle'

Fetching JSON data

Here is a custom React hook to asynchronously fetch JSON data from the web.

import { useQuery } from '@tanstack/react-query';

const fetchJson = async (url) => {
  const response = await fetch(url);
  if (!response.ok) throw response;
  return response.json();
};

export const useJsonQuery = (url) => {
  const { data, isLoading, error } = useQuery({
    queryKey: [url],
    queryFn: () => fetchJson(url)
  });

  return [ data, isLoading, error ];
};

Related links

Required libraries

This code requires the React Query library.

npm install @tanstack/react-query

Notes

fetchJson() is an asynchronous function that takes a URL and returns a Promise that will eventually contain JSON data from that URL. It has to be asynchronous because it calls two asynchronous functions: fetch(), and the Response method Response.json().

useJsonQuery() is a synchronous function that uses the React Query function useQuery() to manage getting data with fetchJson(). useQuery() returns a large object with additional information that this code ignores. useJsonQuery() returns an array of the three most commonly used values:

  • data: the data fetched, if any, or null if no data available yet; initially undefined
  • isLoading: false if loading is complete; initially true
  • error: an error object if something went wrong; initially null

useQuery(), and hence, useJsonQuery(), can only be called inside a component that is nested inside a QueryClientProvider component.

Using fetched data

Here is an app that fetches and displays a list of user names.

import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { useJsonQuery } from './utilities/fetch';

const Main = () => {
  const [data, isLoading, error] = useJsonQuery('https://dummyjson.com/users');

  if (error) return <h1>Error loading user data: {`${error}`}</h1>;
  if (isLoading) return <h1>Loading user data...</h1>;
  if (!data) return <h1>No user data found</h1>;

  return data.users.map(user => <div key={user.id}>{user.firstName} {user.lastName}</div>);
}

const queryClient = new QueryClient();

const App = () => (
  <QueryClientProvider client={queryClient}>
    <div className="container">
      <Main />
    </div>
  </QueryClientProvider>
);

export default App;

Related links

Required libraries

This code requires the React Query library.

npm install @tanstack/react-query

Notes

This demonstrates the value of the React Query library. Instead of having to deal with promises, UserLoader just has to test and display the current results of the data fetch. Either there was an error, the data is still loading, or the data is ready to display. When the results change, e.g, the data is loaded, React will re-render UserLoader.

One instance of a QueryClient must be created and passed to other components by nesting them inside a QueryClientProvider component. The client can be created outside the code that renders the App component.

Fetching data from multiple sources

Here is a custom React hook that fetches JSON data from multiple sources in parallel and returns them as a single object.

...
const fetchJsons = async (obj) => {
  // fetch
  const urls = Object.values(obj);
  const jsons = await Promise.all(urls.map(url => fetchJson(url)));

  // collect into obj
  const keys = Object.keys(obj);
  return Object.fromEntries(keys.map((key, i) => [key, jsons[i]]));
}

export const useJsonQueries = (obj) => {
  const entries = Object.entries(obj);
  const { data, isLoading, error } = useQuery({
    queryKey: entries,
    queryFn: () => fetchJsons(obj))
  });
  return [ data, isLoading, error ];
};

Related links

Required libraries

This code requires the React Query library.

npm install @tanstack/react-query

Notes

fetchJsons() takes an object that has keys and URLs. It returns a promise that resolves to an object with the same keys but the values are the JSON returned for the corresponding URL.

Promise.all() takes an array of promises and returns a single promise that is resolved when all of the input promises resolved. The single promise will hold an array of the results, in the same order as the input promises. Using Promise.all() means that the fetches are done in parallel. The time to do all the promises is the time it takes to do the longest promise, rather than the sum of the times.

Using data fetched from multiple sources

Here is an app that fetches two different sets of data (user and posts) in parallel, and then displays the title and author of each post.

import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { useJsonQueries } from './utilities/fetch';

const Main = () => {
  const [data, isLoading, error] = useJsonQueries({
    userData: 'https://dummyjson.com/users',
    postData: 'https://dummyjson.com/posts'
  });

  if (error) return <h1>Error loading data: {`${error}`}</h1>;
  if (isLoading) return <h1>Loading data...</h1>;
  if (!data) return <h1>No data found</h1>;

  const users = data.userData.users;
  const posts = data.postData.posts;

  return posts.map((post) => {
    const user = users.find(user => user.id === post.userId);
    const author = user ? `${user.firstName} ${user.lastName}` : 'Unknown author';
    return  <div key={post.id}>{post.title} -- {author}</div>;
  });
};

const queryClient = new QueryClient();

const PostApp = () => (
  <QueryClientProvider client={queryClient}>
    <div className="container">
      <Main />
    </div>
  </QueryClientProvider>
);

export default PostApp;

Related links

Required libraries

This code requires the React Query library.

npm install @tanstack/react-query

Notes

A single data object is returned, with the JSON for the posts stored under posts and the JSON for the users stored under users.

The array method find() is used to find the user for the ID listed in the post. A template string is used to construct a string with the user's name. If no user with the given ID is found, a default string is used.

React state with a list

Here is a product page component that manages a state variable with a list of selected products.

import { useState } from 'react';
import ProductList from "./ProductList";

const ProductPage = ({products}) => {
  const [selected, setSelected] = useState([]);

  const toggleSelected = (item) => setSelected(
    selected.includes(item)
    ? selected.filter(x => x !== item)
    : [...selected, item]
  );

  return (
    <ProductList products={products} selected={selected} toggleSelected={toggleSelected} />
  );
};

export default ProductPage;

Related links

Notes

The function toggleSelected() simplifies selecting and unselecting items. If toggleSelected() is passed a product already in the list, it returns a new list without the product. If it passed a product not in the list, it returns a new list with the product added. It's important for React that new lists be created. A state object should never be destructively mutated. Calling the array method splice() would confuse React's update algorithm.

Displaying a selectable list

Here is code for an interface that lets a user select and unselect items from a list of products.

const ProductList = ({products, selected, toggleSelected }) => (
  <div className="product-list">
    {
      products.map(product => (
        <Product key={product.id} product={product} selected={selected} toggleSelected={toggleSelected} />
      ))
    }
  </div>
);
const Product = ({id, product, selected, toggleSelected}) => (
  <div className="product card m-1 p-2" onClick={() => toggleSelected(id)}>
    <img src={product.thumbnail} className="card-img-top" alt={product.title} />
    <div className={`card-body ${selected.includes(id) ? 'selected' : ''}`}>
      <h5 className="card-title">{product.title}</h5>
      <p className="card-text">{product.description}</p>
      <p className="card-text">{formatPrice(product.price)}</p>

    </div>
  </div>
);
.product .selected {
  background-color:lemonchiffon;
  color: green;
}

Related links

Notes

There is no built-in style for a selected card. A CSS class selected is added in Product and a style for that class that changes the text and background colors is used. A more accessible solution would be to add an icon to the card, such as a checkmark.

Defining routes

Here's how to implement a component that renders a different component depending on whether the current URL of the app ends with /posts or /users:

import { BrowserRouter, Routes, Route } from "react-router-dom";
import Posts from './Posts';
import Users from './Users';

const Dispatcher = ({users, posts}) => (
  <BrowserRouter>
    <Routes>
      <Route path="/" element={<Posts posts={posts} users={users} />} />
      <Route path="/users" element={<Users users={users} />} />
    </Routes>
  </BrowserRouter>
);

export default Dispatcher;

A BrowserRouter can be nested inside a page, e.g.,

const Main = () => (
  <div>
    <Header ... />
    <BrowserRouter>
      ...
    </BrowserRouter>
    <Footer ... />
  </div>
);

The BrowserRouter must be high enough in the component hierarchy to surround any components using router functions, such as a navigation bar. You should have just one BrowserRouter component.

Related links

Required libraries

This code uses the React Router library.

npm install react-router-dom

Notes

The Route component associates a URL path with a component. The Routes component then selects which router to render, based on which URL best matches the current URL.

The BrowserRouter component implements "going" to URLs in a browser. It manages the browser's history so that the back button will work as expected with routes. There's also a router for React Native mobile apps, which are not browser-based, and a router for in-memory unit testing.

Defining a navigation bar

The following code creates a navigation bar that lets a user click links to see a page with posts or a page with users. The link for the current page is highlighted.

import { NavLink } from 'react-router-dom';
import 'Navigation.css';

const activation = ({isActive}) => isActive ? 'active' : 'inactive';

const Navigation = () => (
  <nav>
    <NavLink to="/" className={activation}>Posts</NavLink>
    <NavLink to="/users" className={activation}>Users</NavLink>
  </nav>
);

For NavLink to work, the component must be inside your app's BrowserRouter component.

Related links

Required libraries

This code uses the React Router library.

npm install react-router-dom

Notes

A NavLink renders as an HTML link, but clicking it does not load an HTML file. The to attribute specifies the URL. This should match a route that has been defined.

Each NavLink is given a function for its className. React Router calls that function to get the CSS class to use. The function is passed an object with isActive: true if and only if the current URL matches the to URL of the NavLink. The following CSS would make the current link coral and the other links white.

nav {
  background-color: black;
  font-size: 18pt;
  font-family: Arial;
  margin-bottom: 1em;
}
nav a {
  color: white;
  padding: .5em;
  text-decoration: none;
}

nav a.active {
  color: cyan;
}

Defining routes with parameters

Often URLs include data, such as an ID. Here is a code fragment that defines a route such that any URL of the form /users/user-id/edit to goes to UserForm, with the ID and the user with that ID.:

import { BrowserRouter, Routes, Route, useParams } from 'react-router-dom';
import UserForm from './UserForm';
...
const UserFormForUrl = ({users}) => {
  const { id } = useParams();
  return <UserForm id={id} user={users[id]} />;
};

const Dispatcher = ({users, posts}) => (
  <BrowserRouter>
    <Routes>
      ...
      <Route path="/users/:id/edit" element={<UserFormForUrl users={users} />} />
    </Routes>
  </BrowserRouter>
);

export default Dispatcher;

Related links

Required libraries

This code uses the React Router library.

npm install react-router-dom

Notes

Every :name in route path defines a parameter for that route. A path like /users/:id/edit will match a URL such as /users/abc123/edit and associate id to abc123.

The useParams() hook returns a key-value object with the path parameters, if any, and what they matched. For the path /users/:id/edit, useParams() would return an object with the key id.

UserFormForUrl takes care of getting the user to pass to UserForm, so that UserForm can be defined and tested independently of any URL processing.

Here's an example of creating links to specific items by ID. This code displays posts where the poster's name is a link to a page about that user.

import { Link } from 'react-router-dom';
import { firstLastName } from "./User";

const Posts = ({ posts, users }) => (
  <>
    {
      Object.entries(posts).map(([key, post]) => (
        <div key={`post-${key}`}>
          <h3>{post.title}</h3>
          <p>{post.body}</p>
          <p> -- <Link to={`/users/${post.userId}`}>{firstLastName(users[post.userId])}</Link></p>
        </div>
      ))
    }
  </>
);

export default Posts;

Related links

Required libraries

This code uses the React Router library.

npm install react-router-dom

Related links

Notes

A JavaScript template string is used to create the path for to in the Link. Because this is a JavaScript expression, it has to be inside curly braces.

Adding a Firebase Realtime Database

To add the Firebase Realtime Database to an app:

  • enable the database service for the Firebase project in the cloud
  • install the Firebase library in the app
  • add code to initialize firebase in the app

See Hosting an app on a public server for how to create a Firebase project and install the Firebase CLI tools.

To enable the Realtime database for your Firebase project, follow these instructions. When Firebase asks about security, we select Test Mode, so that you can test code to read and store data without setting up authentication. Firebase only allows this mode for a relatively short period of time. See these Firebase notes for information on how to change the security rules later.

To call Firebase functions in an app, install the Firebase library in your app project.

npm install firebase

Then initialize the database.

firebase init database

This adds an entry for the database to your firebase.json file and crestes a database.rules.json file. The rules file is for defining security rules.

Here is the code that needs to be included in the app to initialize interactions with Firebase, including database calls.

import { initializeApp } from 'firebase/app';

const firebaseConfig = {
  apiKey: "...",
  authDomain: "...",
  databaseURL: "...",
  projectId: "...",
  storageBucket: "....",
  messagingSenderId: "...",
  appId: "..."
};

const firebase = initializeApp(firebaseConfig);

To get the firebaseConfig configuration object, go to the project on the Firebase web console. Open Project Overview | Project Settings | General. Scroll down and click on the name of the web app, then click on the Config radio button.

Notes

All calls to Firebase functions are placed into src/utilities/firebase.js for easier upgrading.

Organizing Firebase Realtime data

A Realtime Database is one big JSON object. Here is an example Firebase-friendly JSON object for posts and users. This is a re-organized version of the data at DummyJSON.

{
  "users": {
      "users-1": {
          "id": "users-1",
          "firstName": "Terry",
          "lastName": "Medhurst",
          ...
      },
      "users-2": {
          "id": "users-2",
          "firstName": "Sheldon",
          "lastName": "Quigley",
          ...
      },
      ...
  },
  "posts": {
      "posts-1": {
          "id": "posts-1",
          "title": "His mother had always taught him",
          "body": "His mother had always taught him not to ever think of himself as better than others. He'd tried to live by this motto. He never looked down on those who were less fortunate or who had less money than him. But the stupidity of the group of people he was talking to made him change his mind.",
          "userId": "users-9",
          "tags": [
              "history",
              "american",
              "crime"
          ],
          "reactions": 2
      },
      "posts-2": {
          "id": "posts-2",
          "title": "He was an expert but not in a discipline",
          "body": "He was an expert but not in a discipline that anyone could fully appreciate. He knew how to hold the cone just right so that the soft server ice-cream fell into it at the precise angle to form a perfect cone each and every time. It had taken years to perfect and he could now do it without even putting any thought behind it.",
          "userId": "users-13",
          "tags": [
              "french",
              "fiction",
              "english"
          ],
          "reactions": 2
      },
      ...
  }
}

Notes

Every updatable piece of data has a unique stable path. For example, the age of Sheldon Quiqley will always be /users/user-2/age. Avoid arrays. If an array of users was used, the index to Sheldon might change if users were added or deleted or the list was re-sorted.

Put a list that changes frequently at the top-level so that you can easily track it.

If you have a file with the JSON that works, you can import it into Firebase using the web console.

Read the documentation for how to JSON data to support efficient querying and updating.

Defining a database hook

This code defines two custom React hooks for getting and updating data in a Firebase Realtime database. It would go in the same file as other Firebase-specific code.

import { useEffect, useState } from 'react';
import { getDatabase, onValue, ref, update} from 'firebase/database';
...
const database = getDatabase(firebase);

import { useCallback, useEffect, useState } from 'react';
import { initializeApp } from "firebase/app";
import { getDatabase, onValue, ref, update } from 'firebase/database';

const firebaseConfig = {
  apiKey: "AIzaSyCqy8l97tEWjvW3V1B0f9bMsMLFk9D4sWk",
  authDomain: "maxtactoe.firebaseapp.com",
  databaseURL: "https://maxtactoe.firebaseio.com",
  projectId: "maxtactoe",
  storageBucket: "maxtactoe.appspot.com",
  messagingSenderId: "672040841619",
  appId: "1:672040841619:web:e488e188d5b93db7753866"
};

// Initialize Firebase
const firebase = initializeApp(firebaseConfig);
const database = getDatabase(firebase);

export const useDbData = (path) => {
  const [data, setData] = useState();
  const [error, setError] = useState(null);

  useEffect(() => (
    onValue(ref(database, path), (snapshot) => {
     setData( snapshot.val() );
    }, (error) => {
      setError(error);
    })
  ), [ path ]);

  return [ data, error ];
};

const makeResult = (error) => {
  const timestamp = Date.now();
  const message = error?.message || `Updated: ${new Date(timestamp).toLocaleString()}`;
  return { timestamp, error, message };
};

export const useDbUpdate = (path) => {
  const [result, setResult] = useState();
  const updateData = useCallback((value) => {
    update(ref(database, path), value)
    .then(() => setResult(makeResult()))
    .catch((error) => setResult(makeResult(error)))
  }, [database, path]);

  return [updateData, result];
};

If a component calls useDbData() like this

const [data, error] = useDbData(path);

then data will hold the data at the given location, unless there is an error. If the data changes, data will be updated and the component re-rendered

If a component calls useDbUpdate() like this

const [update, result] = useDbUpate(path);

then update(value) will be a function that can be called to store value at the given location, and result will contain an object with a timestamp for when the update completed, a message describing what happened, and an error object if something went wrong.

Related links

Notes

useDbData() calls the Firebase function onValue() to subscribe to a location, or reference, in the database. The function passed to onValue() will be called with the data at that location, and called again every time there's a change in that data.

The onValue() callback function is passed a snapshot object. The snapshot's val() method will return the actual data.

The useEffect() in useDbData() has path in its dependency array, so that it only creates a subscription when the component is mounted or path changes. The useEffect() returns the return value of onValue(), which is the function to call to unsubscribe. React will call this function when the component getting the data is unmounted. This is needed to avoid zombie listeners running forever.

useDbUpdate() calls the Firebase function update() to update data at database location without affecting keys not included in the value.

Read about useState(), useEffect(), ref(), onValue(), and update(). There are many other functions for getting and changing data. When working with an app that adds items to a list, like posts to a social media site, functions like onChildAdded() and push() are more appropriate than onValue() and update().

Using a database hook

Here's an example of a component that uses useDbData() to get users and posts from a Firebase Realtime database.

import AppRoutes from "./AppRoutes";
import { useDbData } from "../utilities/firebase";

const AppData = () => {
  const [data, error] = useDbData('/');

  if (error) return <h1>Error loading data: {error.toString()}</h1>;
  if (data === undefined) return <h1>Loading data...</h1>;
  if (!data) return <h1>No data found</h1>;

  return <AppRoutes users={data.users} posts={data.posts} />
};

export default AppData;

Related links

Notes

This code gets the entire JSON in the database. If error is not null, something went wrong getting the data. If data is undefined, the data has not been loaded yet. If data is null but there is no error, then there was no data at that location.

Defining a form data hook

Here is an example minimal custom React hook useFormData() to manage form data and validation.

import { useState } from 'react';

export const useFormData = (validator = null, values = {}) => {
  const [state, setState] = useState(() => ({ values }));

  const change = (evt) => {
    const { id, value } = evt.target;
    const error = validator ? validator(id, value) : '';
    evt.target.setCustomValidity(error);
    
    const values = {...state.values, [id]: value};
    const errors = {...state.errors, [id]: error};
    const hasError = Object.values(errors).some(x => x !== '');
    setState(hasError ? { values, errors } : { values });
  };

  return [state, change];
};

Related links

Notes

useFormData() tracks and validates the data in a form. It keeps a state variable with two fields:

  • values: an object containing the form field values
  • errors: an object containing any form field error messages; this key is absent if there are no errors

Both objects use the IDs of the form fields as keys.

useFormData() takes two optional arguments: a validator function and an object with an initial set of form values. If the validator is not null, it must be a function that takes an ID and value and returns a helpful error message when there is a problem with the value. It should return an empty string if there is no problem. useFormData() returns:

  • the state object with the values and errors, if any
  • a change() function that the form should call to update the state.

change() expects to be passed a React form field change event. change() updates the values and error by constructing a new state object.

It is important that change() does not destructive modify the state object.

change() calls the HTML element method setCustomValidity() to set flags that Bootstrap uses in styling form errors.

The errors key is omitted if there are no errors so that form code can see if a form is valid without searching the object.

For more powerful form management, use a library like the react-hook-form library.

Implementing an edit form

This is an example form for a user object. The code uses React controlled components, gives realtime validation feedback, and saves data to Firebase. It uses the useDbUpdate() custom hook and the useFormData() custom hook.

import { useDbUpdate } from '../utilities/firebase';
import { useFormData } from './utilities/useFormData';

const validateUserData = (key, val) => {
  switch (key) {
    case 'firstName': case 'lastName':
      return /(^\w\w)/.test(val) ? '' : 'must be least two characters';
    case 'email':
      return /^\w+@\w+[.]\w+/.test(val) ? '' : 'must contain name@domain.top-level-domain';
    default: return '';
  }
};

const InputField = ({name, text, state, change}) => (
  <div className="mb-3">
    <label htmlFor={name} className="form-label">{text}</label>
    <input className="form-control" id={name} name={name} 
      defaultValue={state.values?.[name]} onChange={change} />
    <div className="invalid-feedback">{state.errors?.[name]}</div>
  </div>
);

const ButtonBar = ({message, disabled}) => {
  const navigate = useNavigate();
  return (
    <div className="d-flex">
      <button type="button" className="btn btn-outline-dark me-2" onClick={() => navigate(-1)}>Cancel</button>
      <button type="submit" className="btn btn-primary me-auto" disabled={disabled}>Submit</button>
      <span className="p-2">{message}</span>
    </div>
  );
};

const UserEditor = ({user}) => {
  const [update, result] = useDbUpdate(`/users/${user.id}`);
  const [state, change] = useFormData(validateUserData, user);
  const submit = (evt) => {
    evt.preventDefault();
    if (!state.errors) {
      update(state.values);
    }
  };

  return (
    <form onSubmit={submit} noValidate className={state.errors ? 'was-validated' : null}>
      <InputField name="firstName" text="First Name" state={state} change={change} />
      <InputField name="lastName" text="Last Name" state={state} change={change} />
      <InputField name="email" text="Email" state={state} change={change} />
      <ButtonBar message={result?.message} />
    </form>
  )
};

export default UserEditor;

Related links

Notes

This form code is an example of React controlled components. That means that the form field values are set by a React state variable. In this case, that variable was created by useFormData(). When the user changes a form field, that change is stored by calling the change() function returned by useFormData().

In React, onChange events are called on every keystroke, like the HTML input event, rather than the HTML change event.

The submit() function is called when the form is submitted. submit() first calls the Event preventDefault() method. The default browser submit response is to send data to a URL, if specified, and reload the page. Reloading page would reset all React state.

submit() calls the update method returned by useDbUpdate() if there are no errors. The update method merges the form data with the corresponding data on Firebase under the path for that specific user.

The Cancel button uses React Router's useNavigate() hook to get a navigate() function. navigate(-1) means "go back one page" so clicking Cancel will return the user to whatever page they were on before going to the editor.

The validateUserData() function that is passed to useFormData() uses a JavaScript switch to choose the validation rule to use, and regular expressions to check that the names have at least two characters (sorry, Mr. T ), and that the email address has an at-sign in it.

This is client-side validation. Client-side validation is important for users, but it does not protect your database from corruption by malicious users or bugs in your validation code. For that, you also need server-side validation.

The components InputField and SubmitButton are used to reduce repetition in the form. These components use Bootstrap 5 classes for styling and form validation to style errors in form field values. Giving quick form validation feedback is the trickiest part of this code. The critical elements that make this happen are:

  • The novalidate on the form to tell the browser not to use built-in form error styles.
  • The was-validated CSS class on the form when there are errors to show.
  • The invalid-feedback CSS class on the div to show error messages, if any.
  • The call to setCustomValidity() in the change() function defined in useFormData().

For more powerful form management, use a library like the react-hook-form library. For common mistakes using controlled components, see the first section of Simple React mistakes.

Resources

Bootstrap forms:

Enabling Google authentication

Here's how to enable authentication with Google, i.e., that users can click on a button and sign in with Google. This avoids the need for a separate database of passwords for an app.

  • Go to the project page for your app on the Firebase web console.
  • Click Authentication on the left under Build product category.
  • Click the Sign-in method tab along the top.
  • Click the Google line.
  • In the panel that opens up, click Enable and enter any required information.
  • Click Save.

Notes

See the section on implementing a sign in button for code using Google authentication.

There are other options you can enable. Email-based authentication is not hard to set up. Other third-party services like Facebook or Twitter will require you to set up a developer account and get an ID for your app.

Defining an authentication hook

Here are functions to sign in and out with Google, and a custom React hook to track when a user signs in or out.

import { getAuth, GoogleAuthProvider, onAuthStateChanged, signInWithPopup, signOut } from 'firebase/auth';
...
export const signInWithGoogle = () => {
  signInWithPopup(getAuth(firebase), new GoogleAuthProvider());
};

const firebaseSignOut = () => signOut(getAuth(firebase));

export { firebaseSignOut as signOut };

export const useAuthState = () => {
  const [user, setUser] = useState();
  
  useEffect(() => (
    onAuthStateChanged(getAuth(firebase), setUser)
  ), []);

  return [user];
};

Related links

Notes

Calling signInWithGoogle pops up a dialog box that lets a user sign in with Google. It uses the general signInWithPopup() function that takes an "auth object" that holds data about the state of authentication, and an authentication provider. In this case, the code passes it the Google authentication provider.

useAuthState() is a custom React hook that returns a state variable containing the current user, if any. Whenever the user signs in or out, the state variable will be updated and the component that called useAuthState() will be re-rendered. If user is not null, it will be a Firebase UserInfo object with a UID (user ID), display name, and email address. The UID is a unique string managed by the authentication provider (Google, Facebook, ...).

It's important that the useEffect() in useAuthState() returns the "unsubscribe" value returned by onAuthStateChanged().

For more on the available authentication functions, see the Firebase authentication page.

Implementing signing in and out

The following code adds a button to sign in or out to a navigation bar. Which button is used depends on whether there is a currently authenticated user or not.

import { NavLink } from 'react-router-dom';
import { signInWithGoogle, signOut, useAuthState } from '../utilities/firebase';

const SignInButton = () => (
  <button className="ms-auto btn btn-dark" onClick={signInWithGoogle}>Sign in</button>
);

const SignOutButton = () => (
  <button className="ms-auto btn btn-dark" onClick={signOut}>Sign out</button>
);

const AuthButton = () => {
  const [user] = useAuthState();
  return user ? <SignOutButton /> : <SignInButton />;
};

const activation = ({isActive}) => isActive ? 'active' : 'inactive';

const Navigation = () => (
  <nav className="d-flex">
    <NavLink to="/" className={activation} end>Posts</NavLink>
    <NavLink to="/users" className={activation} end>Users</NavLink>
    <AuthButton />
  </nav>
);

export default Navigation;

Related links

Notes

The button layout is designed to put the authentication buttons as far away as possible from the navigation links.

CSS classes must be placed on actual DOM elements, like button, not on React components like AuthButton.

Implementing user profiles

Here is a custom React hook that gets additional information from the database about the currently authenticated user, if any. Specifically, this code returns the user and whether they are an administrator. This code could be generalized to include other information about users stored in the database.

import { useAuthState, useDbData } from "./firebase";

export const useProfile = () => {
  const [user] = useAuthState();
  const [isAdmin, isLoading, error] =  useDbData(`/admins/${user?.uid || 'guest'}`);
  return [{ user, isAdmin }, isLoading, error];
};

Related links

Notes

The code always does a database fetch. If there is no user, it uses the dummy ID guest. This is because hooks only work when the pattern of calls matches exactly the pattern of components rendered. The following attempt to avoid a database call would violate the Rules of Hooks:

export const useProfile = () => {
    const [user] = useAuthState();
    let [isAdmin, isLoading, error] = [false, false, null];
    if (user) {
      [isAdmin, isLoading, error] =  useDbData(`/admins/${user?.uid || 'guest'}`);
    }
    return [{ user, isAdmin }, isLoading, error];
  };

Implementing role-based authorization

Here are parts of React Router code to get a user profile and pass it to a component that needs it.

import { useProfile } from '../utilities/profile';
...
const AppRoutes = () => {
  const [profile, profileLoading, profileError] = useProfile();
  ...
  if (profileError) return <h1>Error loading profile: {`${profileError}`}</h1>;
  if (profileLoading) return <h1>Loading user profile</h1>;
  if (!profile) return <h1>No profile data</h1>;
  ...
  return (
    <BrowserRouter>
      ...
      <Routes>
        ...
        <Route path="/users/:id" element={<UserFromUrl profile={profile} users={users} />} />
        ...
      </Routes>
    </BrowserRouter>
  );
};

Here is a User component that includes a button to edit the user information only if the user profile says the current user is an administrator.

import { Link } from 'react-router-dom';

export const firstLastName = (user) => (
  user ? `${user.firstName} ${user.lastName}` : 'Unknown user'
);

const User = ({profile, user}) => (
  <div>
    <h3>
      { firstLastName(user) }  
      { 
        profile?.isAdmin && 
        <Link to={`/users/${user.id}/edit`} style={{fontSize: '0.7em', marginLeft: '2em'}}>
          <i className="bi bi-pencil"></i>
        </Link> }
      </h3>
    <p>Email: {user.email}</p>
    <p>Phone: {user.phone}</p>
  </div>
);

export default User;

Related links

Notes

Always include code to render useful information for all the possible values of state variables. Your code will always be asked to render the initial state values, even if for just a fraction of a second. If something goes wrong, you want tell the user something that they can report to you that will help debugging.

Database security rules

Firebase security rules protect your Firebase data from being modified by unauthorized users. There are two ways to edit your rules:

  • You can go to the Realtime database page for your project on the Firebase web console. Click the Rules tab.
  • You can edit the file database.rules.json in your local repository that was created when you ran firebase init database.

The first option can be handy for testing, but the second option is where you should define your app's permanent security and validation rules. That makes the rules your app needs available to anyone working on the code. Any rules in the local file will overwrite any rules specified on the Firebase console.

Examples of security rules are given below. See the notes that follow for how these rules work.

Test mode access

The following rules let anyone read and change all data. Firebase allows database with rules like this for a short time for early development testing, but will eventually change the rules to deny access.

{
  "rules": {
    ".read": true,
    ".write": true
  }
}

Read-only access

The following rules give everyone read-only access to data stored under the key posts. No other data is accessible and none is writable. Data can be edited on the Firebase web console.

{
  "rules": {
    "posts": {
      ".read": true
    }
  }
}

Authentication-enabled access

The following rule lets anyone posts, and logged-in user creaste posts, i.e., add something under the posts key. No other data is readable or writable.

{
  "rules": {
    "posts": {
      ".read": true,
      ".write": "auth !== null"
    }
  }
}

Identity-enabled access

The following rule does the above, and also lets a logged-in user update their personal user data but no one else's.

{
  "rules": {
    "posts": {
      ".read": true,
      ".write": "auth != null"
    },
    "users": {
      "$uid": {
        ".read": "auth != null && auth.uid == $uid",
        ".write": "auth != null && auth.uid == $uid"
      }
    }
  }
}

Role-based access

The following rules includes the above, but lets an administrator read and write any data.

{
  "rules": {
    ".read": "auth !== null && root.child('admins').child(auth.uid).exists()",
    ".write": "auth !== null && root.child('admins').child(auth.uid).exists()",
    "posts": {
      ".read": true,
      ".write": "auth != null"
    },
    "users": {
      "$uid": {
        ".read": "auth != null && auth.uid == $uid",
        ".write": "auth != null && auth.uid == $uid"
      }
    }
  }
}

The rules above assume that the database includes a top-level key admins under which there are entries with the login ID of every user that is an administrator, like this:

{
  "admins": {
    "user-id": true,
    ...
  },
  "posts": {
  ...
  },
  "users": {
  "user-id": {
    ...
  },
  ...
}

Related links

Notes

The user ID under admins and elsewhere need to be the UIDs managed by the authentication provider. If you are creating the admins manually, and Firebase is providing authentication, then you can get the UIDs from the Firebase web console, under Build | Authentication | Users. There is a "copy UID" button to make easy to copy the UID for placing elsewhere.

It takes time to get the hang of Firebase security rules. The page for editing rules has a Rules Playground that lets you test different request situations to see if your rules allow and deny what you want, before you publish and make them real.

A path to read or write data is evaluated from left to right. A path is given access as soon as "true" is found for the given operation (read or write). A path is denied access if the tree does not have a key for the next element of the path, or the path has been exhausted without finding "true".

The variable auth in a security rule will be set to a UserInfo object if the user has logged in.

A key in a security rule that begins with a dollar sign ($) is a variable. It will be set to the corresponding element being processed in the request path. Typically this is used with things like user IDs. If user data is stored under the user ID provided by your authentication provider, then the example rule above would be true if the user ID in the path matches the ID of the logged-in user.

Role-based access can be implemented using functions that access other parts of the database. The variable root has a RuleDataSnapshot of the root of the database. The snapshot child(path) method returns the snapshot under path. The exists() returns true if there is data for that key. The val() method returns the data in the snapshot. exists() is the same as val() != null.

Server-side data validation

Here is an example of a Firebase server-side validation rule for posts that requires a non-empty title and body and a new post id. Validation rules appear in the same JSON tree as the database security rules.

{
  "posts": {
    ".read": true,
    "$postId": {
      ".write": "auth !== null && !data.exists()",
      ".validate": "newData.child('title').val().length > 0 && newData.child('body').val().length > 0"
    }
  }
}

Notes

Client-side validation of form data is important for users, but it does not replace server-side data validation. Without server-side validation it is very easy for your database to be corrupted and user data lost forever. This can be either accidentally caused by a bug in your client-side code, or intentionally caused by hackers bypassing your HTML form and submitting data via JavaScript directly.

Validation and security rules work together. Both must pass for data to be written.

The predefined variable data is bound to the data stored in the current location accessed by the path processed so far. In the example, !data.exists() is true only if this is a new post id.

The predefined variable newData refers to the data about to be stored. The first two expressions in the .validate rule test that there's data in the title and body. The last expression takes the user ID in the post and verifies that that user exists in the user

See Firebase validation rules and this collection of sample validation rules.

© 2024 Chris Riesbeck
Template design by Andreas Viklund