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