Material UI with Astro and React

Nov 03, 2022 Material UI with Astro and React

MUI and Tailwind CSS is my top pick when creating a web project. And when I tried Astro for the first time, I felt like a free bird who can fly anywhere across JavaScript frameworks.

So, I tried to run MUI with Astro and found some limitations that are yet to support. Astro has options for how your UI components will load on the browser called hydration directives.

For now, you can only use MUI with the client:only directive. In the future, they might update the React integration package or add a new integration to support the other hydration directives.

You can follow this issue for more info and get updates. Or you can follow me on Twitter to get any updates on this post.

Creating the project

Let’s create an Astro project first with the empty project template and Strict typescript option first.

# Using NPM
npm create astro@latest
# Using Yarn
yarn create astro
# Using PNPM
pnpm create astro@latest

This will create an Astro project without any heavy templates.

Now, Let’s add React integration to our project.

# Using NPM
npx astro add react
# Using Yarn
yarn astro add react
# Using PNPM
pnpm astro add react

It will ask for permission to install packages and update astro.config.mjs and tsconfig.json files. Press y on your keyboard every time when it asks to continue the installation.

After the installation is completed, create a components folder, create a Home.tsx file inside the folder and then paste the code below.

export default function Home() {
  return (
    <div>
      <h1>Hello World</h1>
    </div>
  );
}

Also, update the index.astro by importing your Home component to test if the React integration working perfectly.

---
import Home from "../components/Home";
---

<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" />
		<meta name="generator" content={Astro.generator} />
		<title>Astro</title>
	</head>
	<body>
		<Home />
	</body>
</html>

Now, restart your server if it’s already running and run the npm dev command and see if your project running successfully.

Astro react hello world empty project

Installing MUI packages

I am assuming you have at least basic knowledge about Material UI and React. If you are trying Material UI for the first time, I suggest you learn Material UI with React first.

Now let’s install all the necessary packages for Material UI including fonts, icons, etc.

# Using NPM
npm install @mui/material @mui/styled-engine-sc styled-components @fontsource/roboto @mui/icons-material
# Using YARN
yarn add @mui/material @mui/styled-engine-sc styled-components @fontsource/roboto @mui/icons-material
# Using PNPM
pnpm add @mui/material @mui/styled-engine-sc styled-components @fontsource/roboto @mui/icons-material

Material UI components with Astro

We are going to make a single to-do form. So, Let’s paste the code below in Home.tsx file and let me explain step by step.

import { ThemeProvider } from "@emotion/react";
import {
  Box,
  Button,
  Container,
  CssBaseline,
  TextField,
  Typography,
} from "@mui/material";
import { purple } from "@mui/material/colors";
import { createTheme } from "@mui/material/styles";
import "@fontsource/roboto/300.css";
import "@fontsource/roboto/400.css";
import "@fontsource/roboto/500.css";
import "@fontsource/roboto/700.css";

const theme = createTheme({
  palette: {
    primary: {
      main: purple[500],
    },
  },
});

export default function Home() {
	const handleTodo = (e: React.SyntheticEvent) => {
    e.preventDefault();
    const target = e.target as typeof e.target & {
      todo: { value: string };
    };
    alert(target.todo.value);
  };
  return (
    <ThemeProvider theme={theme}>
      <CssBaseline />
      <Container maxWidth="xs" sx={{ my: 10 }}>
        <Typography variant="h4" fontWeight={600} gutterBottom>
          Add Todo
        </Typography>
        <Box
          component="form"
          sx={{ display: "flex", flexDirection: "column", gap: 2 }}
          onSubmit={handleTodo}
        >
          <TextField id="todo" name="todo" label="Add a Task" />
          <Button variant="contained" type="submit">
            Add Task
          </Button>
        </Box>
      </Container>
    </ThemeProvider>
  );
}

Since Material UI depends on Roboto font by default we imported Roboto fonts. Then we created a theme config to modify the primary theme color to test if the ThemeProvider component is working.

Then, we added Container, Typography, Box, TextFieldand Button components to create our add-to-do form which will open up on the alert box with the input value.

Now, Let’s update our index.astro file to add hydration directive client:only which will render the component on the client side when the page loads.

---
import Home from "../components/Home";
---

<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" />
		<meta name="generator" content={Astro.generator} />
		<title>Astro</title>
	</head>
	<body>
		<Home client:only="react" />
	</body>
</html>

Save your files and check your browser to see if everything is working like below. Make sure you restarted your dev server after installing the MUI npm packages.

Astro react to do form material ui

As you can see, the primary color of the button is changed because of the ThemeProvider component.

State Management with Material UI, Astro, and React

Usually, we use React state hooks such as useState to update the component state, for example, opening/closing Modal, Dialogue, Drawer, etc.

But Astro suggests a different solution to share the state between your components. With Nano Stores, you can share your state across multiple UI frameworks. It is also very lightweight.

Let’s see how we can use Nano Store with our Material UI components. But first, we have to install the npm packages.

# Using NPM
npm install nanostores @nanostores/react
# Using YARN
yarn add nanostores @nanostores/react
# Using PNPM
pnpm add nanostores @nanostores/react

Now, create a new file stores/todoStore.ts in the src folder. and paste the code below.

import { atom } from "nanostores";

export interface Todo {
  title: string;
}
export const todosAtom = atom<Todo[]>([]);

Here we created an interface and atom for Todo. The Atom store is to store your data. Compared to [state, setState] it has todosAtom.get() and todosAtom.set()methods to get and store the data. Nano Store has another hook called useStore to only get the data.

Let’s update our Home.tsx file too.

import { ThemeProvider } from "@emotion/react";
import {
  Box,
  Button,
  Container,
  CssBaseline,
  Paper,
  TextField,
  Typography,
} from "@mui/material";
import { purple } from "@mui/material/colors";
import { createTheme } from "@mui/material/styles";
import "@fontsource/roboto/300.css";
import "@fontsource/roboto/400.css";
import "@fontsource/roboto/500.css";
import "@fontsource/roboto/700.css";
import { useStore } from "@nanostores/react";
import { todosAtom } from "../stores/todoStore";

const theme = createTheme({
  palette: {
    primary: {
      main: purple[500],
    },
  },
});

export default function Home() {
  const todos = useStore(todosAtom);

  const handleTodo = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    const target = e.target as typeof e.target & {
      todo: { value: string };
      reset: () => void; // clear all input values in the form
    };
    const newTodo = { title: target.todo.value };
    todosAtom.set(todos.concat(newTodo));

    target.reset();
  };

  return (
    <ThemeProvider theme={theme}>
      <CssBaseline />
      <Container maxWidth="xs" sx={{ my: 10 }}>
        <Typography variant="h4" fontWeight={600} gutterBottom>
          Add Todo
        </Typography>
        <Box
          component="form"
          sx={{ display: "flex", flexDirection: "column", gap: 2 }}
          onSubmit={handleTodo}
        >
          <TextField id="todo" name="todo" label="Add a Task" />
          <Button variant="contained" type="submit">
            Add Task
          </Button>
        </Box>
        <Box sx={{ my: 4 }}>
          <Typography variant="h5" gutterBottom fontWeight={600}>
            List of To Do's
          </Typography>
          <Box>
            {todos.map((todo, index) => (
              <Paper key={index} sx={{ px: 2, py: 1, mb: 2 }}>
                {todo.title}
              </Paper>
            ))}
          </Box>
        </Box>
      </Container>
    </ThemeProvider>
  );
}

As you can see, we are getting the value by using the useStore hook from Nano Store. Then, we are updating our state by adding new to-do. To clear the form input values, we are using the reset() method. And below, we are returning our to-dos inside the Paper component.

Try adding to-do’s and you should see the same as below.

List of to dos added by material ui form with astro react

Try running npm run build and npm run preview to see if your application has any issues with running builds and preview.

Conclusion

You can try other Material UI components like Drawers, Table, etc. But instead of the useState hook, I would highly recommend using Nano Store. Because you may have to manage your state in the Astro component too.

In my opinion, you shouldn’t use MUI with Astro in production since there are still lots of issues to fix. The main reason I would use Astro is to ship with zero JavaScript which is not possible with Material UI yet.