7.8.0

mantinedev/mantine7.8.0Apr 12, 2024by rtivital

AI Summary

Performance-focused release introducing 'uncontrolled mode' for forms to reduce re-renders. Adds new hooks `use-fetch` and `use-map`, along with PostCSS `autoRem` option.

Key Highlights

  • Uncontrolled form mode introduced for performance improvement
  • New `form.getValues()` method to access current values
  • New `form.watch()` effect function for field subscriptions
  • New `use-fetch` hook for data fetching
  • New `use-map` hook for map data management

New Features

  • Uncontrolled form mode
  • form.getValues() method
  • form.watch() method
  • use-fetch hook
  • use-map hook
  • PostCSS autoRem option

Full Release Notes

[View changelog with demos on mantine.dev website](https://mantine.dev/changelog/7-8-0)

## Auto convert px to rem in .css files

Start from version `1.14.4` [postcss-preset-mantine](https://mantine.dev/styles/postcss-preset)
supports `autoRem` option that can be used to automatically convert all `px` values
to `rem` units in `.css` files.

```js
module.exports = {
  plugins: {
    'postcss-preset-mantine': {
      autoRem: true,
    },
  },
};
```

This option works similar to `rem` function. The following code:

```scss
.demo {
  font-size: 16px;

  @media (min-width: 320px) {
    font-size: 32px;
  }
}
```

Will be transformed to:

```scss
.demo {
  font-size: calc(1rem * var(--mantine-scale));

  @media (min-width: 320px) {
    font-size: calc(2rem * var(--mantine-scale));
  }
}
```

Note that `autoRem` converts only CSS properties, values in `@media` queries are
not converted automatically – you still need to use `em` function to convert them.

`autoRem` option does not convert values in the following cases:

- Values in `calc()`, `var()`, `clamp()` and `url()` functions
- Values in `content` property
- Values that contain `rgb()`, `rgba()`, `hsl()`, `hsla()` colors

If you want to convert above values to rem units, use `rem` function manually.

## Uncontrolled form mode

[useForm](https://mantine.dev/form/use-form) hook now supports [uncontrolled mode](https://mantine.dev/form/uncontrolled).
Uncontrolled mode provides a significant performance improvement by reducing
the number of re-renders and the amount of state updates almost to 0. Uncontrolled
mode is now the recommended way to use the `useForm` hook for almost all use cases.

Example of uncontrolled form (`form.values` are not updated):

```tsx
import { useState } from 'react';
import { Button, Code, Text, TextInput } from '@mantine/core';
import { hasLength, isEmail, useForm } from '@mantine/form';

function Demo() {
  const form = useForm({
    mode: 'uncontrolled',
    initialValues: { name: '', email: '' },
    validate: {
      name: hasLength({ min: 3 }, 'Must be at least 3 characters'),
      email: isEmail('Invalid email'),
    },
  });

  const [submittedValues, setSubmittedValues] = useState<typeof form.values | null>(null);

  return (
    <form onSubmit={form.onSubmit(setSubmittedValues)}>
      <TextInput {...form.getInputProps('name')} label="Name" placeholder="Name" />
      <TextInput {...form.getInputProps('email')} mt="md" label="Email" placeholder="Email" />
      <Button type="submit" mt="md">
        Submit
      </Button>

      <Text mt="md">Form values:</Text>
      <Code block>{JSON.stringify(form.values, null, 2)}</Code>

      <Text mt="md">Submitted values:</Text>
      <Code block>{submittedValues ? JSON.stringify(submittedValues, null, 2) : '–'}</Code>
    </form>
  );
}
```

## form.getValues

With uncontrolled mode, you can not access `form.values` as a state variable,
instead, you can use `form.getValues()` method to get current form values at any time:

```tsx
import { useForm } from '@mantine/form';

const form = useForm({
  mode: 'uncontrolled',
  initialValues: { name: 'John Doe' },
});

form.getValues(); // { name: 'John Doe' }

form.setValues({ name: 'John Smith' });
form.getValues(); // { name: 'John Smith' }
```

`form.getValues()` always returns the latest form values, it is safe to use it
after state updates:

```tsx
import { useForm } from '@mantine/form';

const form = useForm({
  mode: 'uncontrolled',
  initialValues: { name: 'John Doe' },
});

const handleNameChange = () => {
  form.setFieldValue('name', 'Test Name');

  // ❌ Do not use form.values to get the current form values
  // form.values has stale name value until next rerender in controlled mode
  // and is always outdated in uncontrolled mode
  console.log(form.values); // { name: 'John Doe' }

  // ✅ Use form.getValues to get the current form values
  // form.getValues always returns the latest form values
  console.log(form.getValues()); // { name: 'Test Name' }
};
```

## form.watch

`form.watch` is an effect function that allows subscribing to changes of a
specific form field. It accepts field path and a callback function that is
called with new value, previous value, touched and dirty field states:

```tsx
import { TextInput } from '@mantine/core';
import { useForm } from '@mantine/form';

function Demo() {
  const form = useForm({
    mode: 'uncontrolled',
    initialValues: {
      name: '',
      email: '',
    },
  });

  form.watch('name', ({ previousValue, value, touched, dirty }) => {
    console.log({ previousValue, value, touched, dirty });
  });

  return (
    <div>
      <TextInput label="Name" placeholder="Name" {...form.getInputProps('name')} />
      <TextInput mt="md" label="Email" placeholder="Email" {...form.getInputProps('email')} />
    </div>
  );
}
```

## Customize Popover middlewares

You can now customize `middlewares` options in [Popover](https://mantine.dev/core/popover) component and
in other components ([Menu](https://mantine.dev/core/menu), [Select](https://mantine.dev/core/select), [Combobox](https://mantine.dev/core/combobox), etc.)
based on Popover.

To customize [Floating UI](https://floating-ui.com/) middlewares options, pass them as
an object to the `middlewares` prop. For example, to change [shift](https://floating-ui.com/docs/shift)
middleware padding to `20px` use the following configuration:

```tsx
import { Popover } from '@mantine/core';

function Demo() {
  return (
    <Popover middlewares={{ shift: { padding: 20 } }} position="bottom">
      {/* Popover content */}
    </Popover>
  );
}
```

## use-fetch hook

New [use-fetch](https://mantine.dev/hooks/use-fetch) hook:

```tsx
import { Box, Button, Code, Group, LoadingOverlay, Text } from '@mantine/core';
import { useFetch } from '@mantine/hooks';

interface Item {
  userId: number;
  id: number;
  title: string;
  completed: boolean;
}

function Demo() {
  const { data, loading, error, refetch, abort } = useFetch<Item[]>(
    'https://jsonplaceholder.typicode.com/todos/'
  );

  return (
    <div>
      {error && <Text c="red">{error.message}</Text>}

      <Group>
        <Button onClick={refetch} color="blue">
          Refetch
        </Button>
        <Button onClick={abort} color="red">
          Abort
        </Button>
      </Group>
      <Box pos="relative" mt="md">
        <Code block>{data ? JSON.stringify(data.slice(0, 3), null, 2) : 'Fetching'}</Code>
        <LoadingOverlay visible={loading} />
      </Box>
    </div>
  );
}
```

## use-map hook

New [use-map](https://mantine.dev/hooks/use-map) hook:

```tsx
import { IconPlus, IconTrash } from '@tabler/icons-react';
import { ActionIcon, Group, Table } from '@mantine/core';
import { useMap } from '@mantine/hooks';

function Demo() {
  const map = useMap([
    ['/hooks/use-media-query', 4124],
    ['/hooks/use-clipboard', 8341],
    ['/hooks/use-fetch', 9001],
  ]);

  const rows = Array.from(map.entries()).map(([key, value]) => (
    <Table.Tr key={key}>
      <Table.Td>{key}</Table.Td>
      <Table.Td>{value}</Table.Td>
      <Table.Td>
        <Group>
          <ActionIcon variant="default" onClick={() => map.set(key, value + 1)} fw={500}>
            <IconPlus stroke={1.5} size={18} />
          </ActionIcon>
          <ActionIcon variant="default" onClick={() => map.delete(key)} c="red">
            <IconTrash stroke={1.5} size={18} />
          </ActionIcon>
        </Group>
      </Table.Td>
    </Table.Tr>
  ));

  return (
    <Table layout="fixed">
      <Table.Thead>
        <Table.Tr>
          <Table.Th>Page</Table.Th>
          <Table.Th>Views last month</Table.Th>
          <Table.Th />
        </Table.Tr>
      </Table.Thead>
      <Table.Tbody>{rows}</Table.Tbody>
    </Table>
  );
}
```

## use-set hook

New [use-set](https://mantine.dev/hooks/use-set) hook:

```tsx
import { useState } from 'react';
import { Code, Stack, TextInput } from '@mantine/core';
import { useSet } from '@mantine/hooks';

function Demo() {
  const [input, setInput] = useState('');
  const scopes = useSet<string>(['@mantine', '@mantine-tests', '@mantinex']);

  const isDuplicate = scopes.has(input.trim().toLowerCase());

  const items = Array.from(scopes).map((scope) => <Code key={scope}>{scope}</Code>);

  return (
    <>
      <TextInput
        label="Add new scope"
        placeholder="Enter scope"
        description="Duplicate scopes are not allowed"
        value={input}
        onChange={(event) => setInput(event.currentTarget.value)}
        error={isDuplicate && 'Scope already exists'}
        onKeyDown={(event) => {
          if (event.nativeEvent.code === 'Enter' && !isDuplicate) {
            scopes.add(input.trim().toLowerCase());
            setInput('');
          }
        }}
      />

      <Stack gap={5} align="flex-start" mt="md">
        {items}
      </Stack>
    </>
  );
}
```

## use-debounced-callback hook

New [use-debounced-callback](https://mantine.dev/hooks/use-debounced-callback) hook:

```tsx
import { useState } from 'react';
import { Loader, Text, TextInput } from '@mantine/core';
import { useDebouncedCallback } from '@mantine/hooks';

function getSearchResults(query: string): Promise<{ id: number; title: string }[]> {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve(
        query.trim() === ''
          ? []
          : Array(5)
              .fill(0)
              .map((_, index) => ({ id: index, title: `${query} ${index + 1}` }))
      );
    }, 1000);
  });
}

function Demo() {
  const [search, setSearch] = useState('');
  const [searchResults, setSearchResults] = useState<{ id: number; title: string }[]>([]);
  const [loading, setLoading] = useState(false);

  const handleSearch = useDebouncedCallback(async (query: string) => {
    setLoading(true);
    setSearchResults(await getSearchResults(query));
    setLoading(false);
  }, 500);

  const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    setSearch(event.currentTarget.value);
    handleSearch(event.currentTarget.value);
  };

  return (
    <>
      <TextInput
        value={search}
        onChange={handleChange}
        placeholder="Search..."
        rightSection={loading && <Loader size={20} />}
      />
      {searchResults.map((result) => (
        <Text key={result.id} size="sm">
          {result.title}
        </Text>
      ))}
    </>
  );
}
```

## use-throttled-state hook

New [use-throttled-state](https://mantine.dev/hooks/use-throttled-state) hook:

```tsx
import { Text, TextInput } from '@mantine/core';
import { useThrottledState } from '@mantine/hooks';

function Demo() {
  const [throttledValue, setThrottledValue] = useThrottledState('', 1000);

  return (
    <>
      <TextInput
        placeholder="Search"
        onChange={(event) => setThrottledValue(event.currentTarget.value)}
      />
      <Text>Throttled value: {throttledValue || '–'}</Text>
    </>
  );
}
```

## use-throttled-value hook

New [use-throttled-value](https://mantine.dev/hooks/use-throttled-value) hook:

```tsx
import { Text, TextInput } from '@mantine/core';
import { useThrottledValue } from '@mantine/hooks';

function Demo() {
  const [value, setValue] = useState('');
  const throttledValue = useThrottledValue(value, 1000);

  return (
    <>
      <TextInput placeholder="Search" onChange={(event) => setValue(event.currentTarget.value)} />
      <Text>Throttled value: {throttledValue || '–'}</Text>
    </>
  );
}
```

## use-throttled-callback hook

New [use-throttled-callback](https://mantine.dev/hooks/use-throttled-callback) hook:

```tsx
import { Text, TextInput } from '@mantine/core';
import { useThrottledCallback } from '@mantine/hooks';

function Demo() {
  const [throttledValue, setValue] = useState('');
  const throttledSetValue = useThrottledCallback((value) => setValue(value), 1000);

  return (
    <>
      <TextInput
        placeholder="Search"
        onChange={(event) => throttledSetValue(event.currentTarget.value)}
      />
      <Text>Throttled value: {throttledValue || '–'}</Text>
    </>
  );
}
```

## use-orientation hook

New [use-orientation](https://mantine.dev/hooks/use-orientation) hook:

```tsx
import { Code, Text } from '@mantine/core';
import { useOrientation } from '@mantine/hooks';

function Demo() {
  const { angle, type } = useOrientation();
  return (
    <>
      <Text>
        Angle: <Code>{angle}</Code>
      </Text>
      <Text>
        Type: <Code>{type}</Code>
      </Text>
    </>
  );
}
```

## use-is-first-render hook

New [use-is-first-render](https://mantine.dev/hooks/use-is-first-render) hook:

```tsx
import { useState } from 'react';
import { Button, Text } from '@mantine/core';
import { useIsFirstRender } from '@mantine/hooks';

function Demo() {
  const [counter, setCounter] = useState(0);
  const firstRender = useIsFirstRender();
  return (
    <div>
      <Text>
        Is first render:{' '}
        <Text span c={firstRender ? 'teal' : 'red'}>
          {firstRender ? 'Yes' : 'No!'}
        </Text>
      </Text>
      <Button onClick={() => setCounter((c) => c + 1)} mt="sm">
        Rerendered {counter} times, click to rerender
      </Button>
    </div>
  );
}
```

## Documentation updates

- New [uncontrolled form](https://mantine.dev/form/uncontrolled) guide
- [onValuesChange](https://mantine.dev/form/values/#onvalueschange) documentation has been added
- A new demo has been added to [tiptap](https://mantine.dev/x/tiptap/#typography-styles) that shows how to customize typography styles
- A new guide has been added to customize [Popover](https://mantine.dev/core/popover/#customize-middleware-options) middlewares

## Other changes

- [NumberInput](https://mantine.dev/core/number-input) now supports `withKeyboardEvents={false}` to disable up/down arrow keys handling
- [Popover](https://mantine.dev/core/popover) [shift](https://floating-ui.com/docs/shift) middleware now has default padding of 5px to offset dropdown near the edge of the viewport