Skip to content

Commit

Permalink
Merge pull request #7051 from marmelab/resourceless-reducers
Browse files Browse the repository at this point in the history
[BC Break] Remove Resource initialization
  • Loading branch information
djhi authored Jan 4, 2022
2 parents 833cec8 + d9378a3 commit ef24549
Show file tree
Hide file tree
Showing 52 changed files with 786 additions and 799 deletions.
120 changes: 118 additions & 2 deletions UPGRADE.md
Original file line number Diff line number Diff line change
Expand Up @@ -809,6 +809,56 @@ const App = () => {
}
```

## Custom Menus Should Get Resource Definition From Context

React-admin used to store the definitino of each resource (its name, icon, label, etc.) in the Redux state. This is no longer the case, as the resource definition is now stored in a custom context.

If you relied on the `useResourceDefinition` hook, this change shouldn't affect you.

If you need to access the definitions of all resources, however, you must upgrade your code, and use the new `useResourceDefinitions` hook.

The most common use case is when you override the default `<Menu>` component:

```diff
// in src/Menu.js
import * as React from 'react';
import { createElement } from 'react';
-import { useSelector } from 'react-redux';
import { useMediaQuery } from '@material-ui/core';
-import { DashboardMenuItem, Menu, MenuItemLink, getResources } from 'react-admin';
+import { DashboardMenuItem, Menu, MenuItemLink, useResourceDefinitions } from 'react-admin';
import DefaultIcon from '@material-ui/icons/ViewList';
import LabelIcon from '@material-ui/icons/Label';

export const Menu = (props) => {
- const resources = useSelector(getResources);
+ const resourcesDefinitions = useResourceDefinitions();
+ const resources = Object.keys(resourcesDefinitions).map(name => resourcesDefinitions[name]);
const open = useSelector(state => state.admin.ui.sidebarOpen);
return (
<Menu {...props}>
<DashboardMenuItem />
{resources.map(resource => (
<MenuItemLink
key={resource.name}
to={`/${resource.name}`}
primaryText={
(resource.options && resource.options.label) ||
resource.name
}
leftIcon={
resource.icon ? <resource.icon /> : <DefaultIcon />
}
onClick={props.onMenuClick}
sidebarIsOpen={open}
/>
))}
{/* add your custom menus here */}
</Menu>
);
};
```

## No More Prop Injection In Page Components

Page components (`<List>`, `<Show>`, etc.) used to expect to receive props (route parameters, permissions, resource name). These components don't receive any props anymore by default. They use hooks to get the props they need from contexts or route state.
Expand Down Expand Up @@ -990,13 +1040,79 @@ const CommentGrid = () => {

## Removed Reducers

React-admin no longer relies on Redux to fetch relationships. Instead, the cache of previously fetched relationships is managed by react-query.
If your code used `useSelector` to read the react-admin application state, it will likely break. React-admin v4 uses Redux much less than v3, and the shape of the Redux state has changed.

React-admin no longer uses Redux for **data fetching**. Instead, it uses react-query. If you used to read data from the Redux store (which was a bad practice by the way), you'll have to use specialized data provider hooks instead.

```diff
import * as React from "react";
-import { useSelector } from 'react-redux';
+import { useGetOne } from 'react-admin';
import { Loading, Error } from '../ui';

const UserProfile = ({ record }) => {
- const data = useSelector(state => state.resources.users.data[record.id]);
+ const { data, isLoading, error } = useGetOne(
+ 'users',
+ { id: record.id }
+ );
+ if (isLoading) { return <Loading />; }
+ if (error) { return <Error />; }
return <div>User {data.username}</div>;
};
```

Besides, the `loadedOnce` reducer, used internally for the previous version of the List page logic, is no longer necessary and has been removed.

React-admin no longer relies on Redux to fetch **relationships**. Instead, the cache of previously fetched relationships is managed by react-query.

If you need to get the records related to the current one via a one-to-many relationship (e.g. to fetch all the books of a given author), you can use the `useGetManyReference` hook instead of the `oneToMany` reducer.

If you need to get possible values for a relationship, use the `useGetList` hook instead of the `possibleValues` reducer.

Besides, the `loadedOnce` reducer, used internally for the previous version of the List page logic, is no longer necessary and has been removed.
React-admin no longer uses Redux for **resource definitions**. Instead, it uses a custom context. If you used the `useResourceDefinition` hook, this change is backwards compatible. But if you used to read the Redux state directly, you'll have to upgrade your code. This often happens for custom menus, using the `getResources` selector:

```diff
// in src/Menu.js
import * as React from 'react';
import { createElement } from 'react';
-import { useSelector } from 'react-redux';
import { useMediaQuery } from '@material-ui/core';
-import { DashboardMenuItem, Menu, MenuItemLink, getResources } from 'react-admin';
+import { DashboardMenuItem, Menu, MenuItemLink, useResourceDefinitions } from 'react-admin';
import DefaultIcon from '@material-ui/icons/ViewList';
import LabelIcon from '@material-ui/icons/Label';

export const Menu = (props) => {
- const resources = useSelector(getResources);
+ const resourcesDefinitions = useResourceDefinitions();
+ const resources = Object.keys(resourcesDefinitions).map(name => resourcesDefinitions[name]);
const open = useSelector(state => state.admin.ui.sidebarOpen);
return (
<Menu {...props}>
<DashboardMenuItem />
{resources.map(resource => (
<MenuItemLink
key={resource.name}
to={`/${resource.name}`}
primaryText={
(resource.options && resource.options.label) ||
resource.name
}
leftIcon={
resource.icon ? <resource.icon /> : <DefaultIcon />
}
onClick={props.onMenuClick}
sidebarIsOpen={open}
/>
))}
{/* add your custom menus here */}
</Menu>
);
};
```

Reducers for the **list parameters** (current sort & filters, selected ids, expanded rows) have moved up to the root reducer (so they don't need the resource to be registered first). This shouldn't impact you if you used the react-admin hooks (`useListParams`, `useSelection`) to read the state.

## Redux-Saga Was Removed

Expand Down
17 changes: 9 additions & 8 deletions docs/Admin.md
Original file line number Diff line number Diff line change
Expand Up @@ -217,21 +217,22 @@ import * as React from 'react';
import { createElement } from 'react';
import { useSelector } from 'react-redux';
import { useMediaQuery } from '@material-ui/core';
import { MenuItemLink, getResources } from 'react-admin';
import { MenuItemLink, useResourceDefinitions } from 'react-admin';
import LabelIcon from '@material-ui/icons/Label';

const Menu = ({ onMenuClick, logout }) => {
const isXSmall = useMediaQuery(theme => theme.breakpoints.down('xs'));
const open = useSelector(state => state.admin.ui.sidebarOpen);
const resources = useSelector(getResources);
const resources = useResourceDefinitions();

return (
<div>
{resources.map(resource => (
{Object.keys(resources).map(name => (
<MenuItemLink
key={resource.name}
to={`/${resource.name}`}
primaryText={resource.options && resource.options.label || resource.name}
leftIcon={createElement(resource.icon)}
key={name}
to={`/${name}`}
primaryText={resources[name].options && resources[name].options.label || name}
leftIcon={createElement(resources[name].icon)}
onClick={onMenuClick}
sidebarIsOpen={open}
/>
Expand Down Expand Up @@ -296,7 +297,7 @@ For more details on predefined themes and custom themes, refer to the [Material

## `layout`

If you want to deeply customize the app header, the menu, or the notifications, the best way is to provide a custom layout component. It must contain a `{children}` placeholder, where react-admin will render the resources. If you use material UI fields and inputs, it should contain a `<ThemeProvider>` element. And finally, if you want to show the spinner in the app header when the app fetches data in the background, the Layout should connect to the redux store.
If you want to deeply customize the app header, the menu, or the notifications, the best way is to provide a custom layout component. It must contain a `{children}` placeholder, where react-admin will render the resources.

Use the [default layout](https://github.com/marmelab/react-admin/blob/master/packages/ra-ui-materialui/src/layout/Layout.tsx) as a starting point, and check [the Theming documentation](./Theming.md#using-a-custom-layout) for examples.

Expand Down
8 changes: 3 additions & 5 deletions docs/Resource.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ In react-admin terms, a *resource* is a string that refers to an entity type (li

A `<Resource>` component has 3 responsibilities:

- It defines the page components to use for interacting with the resource records (to display a list of records, the details of a record, or to create a new one).
- It initializes the internal data store so that react-admin components can see it as a mirror of the API for a given resource.
- It creates a context that lets every descendent component know in which resource they are used (this context is called `ResourceContext`).
- It defines the components for the CRUD routes of a given resource (to display a list of records, the details of a record, or to create a new one).
- It creates a context that lets every descendent component know the current resource name (this context is called `ResourceContext`).
- It stores the resource definition (its name, icon, and label) inside a shared context (this context is called `ResourceDefinitionContext`).

`<Resource>` components can only be used as children of [the `<Admin>` component](./Admin.md).

Expand Down Expand Up @@ -44,8 +44,6 @@ const App = () => (
);
```

**Tip**: You must add a `<Resource>` when you declare a reference (via `<ReferenceField>`, `<ReferenceArrayField>`, `<ReferenceManyField>`, `<ReferenceInput>` or `<ReferenceArrayInput>`), because react-admin uses resources to define the data store structure. That's why there is an empty `tags` resource in the example above.

**Tip**: How does a resource map to an API endpoint? The `<Resource>` component doesn't know this mapping - it's [the `dataProvider`'s job](./DataProviders.md) to define it.

## `name`
Expand Down
18 changes: 9 additions & 9 deletions docs/Theming.md
Original file line number Diff line number Diff line change
Expand Up @@ -853,34 +853,34 @@ const App = () => (
);
```

**Tip**: You can generate the menu items for each of the resources by reading the Resource configurations from the Redux store:
**Tip**: You can generate the menu items for each of the resources by reading the Resource configurations context:

```jsx
// in src/Menu.js
import * as React from 'react';
import { createElement } from 'react';
import { useSelector } from 'react-redux';
import { useMediaQuery } from '@material-ui/core';
import { DashboardMenuItem, Menu, MenuItemLink, getResources } from 'react-admin';
import { DashboardMenuItem, Menu, MenuItemLink, useResourceDefinitions } from 'react-admin';
import DefaultIcon from '@material-ui/icons/ViewList';
import LabelIcon from '@material-ui/icons/Label';

export const Menu = (props) => {
const resources = useSelector(getResources);
const resources = useResourceDefinitions()
const open = useSelector(state => state.admin.ui.sidebarOpen);
return (
<Menu {...props}>
<DashboardMenuItem />
{resources.map(resource => (
{Object.keys(resources).map(name => (
<MenuItemLink
key={resource.name}
to={`/${resource.name}`}
key={name}
to={`/${name}`}
primaryText={
(resource.options && resource.options.label) ||
resource.name
(resources[name].options && resources[name].options.label) ||
name
}
leftIcon={
resource.icon ? <resource.icon /> : <DefaultIcon />
resources[name].icon ? <resource.icon /> : <DefaultIcon />
}
onClick={props.onMenuClick}
sidebarIsOpen={open}
Expand Down
2 changes: 0 additions & 2 deletions packages/ra-core/src/actions/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
export * from './clearActions';
export * from './filterActions';
export * from './listActions';
export * from './localeActions';
export * from './notificationActions';
export * from './resourcesActions';
export * from './uiActions';
export * from './undoActions';
52 changes: 0 additions & 52 deletions packages/ra-core/src/actions/localeActions.ts

This file was deleted.

29 changes: 0 additions & 29 deletions packages/ra-core/src/actions/resourcesActions.ts

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { Form } from 'react-final-form';
import { useReferenceArrayInputController } from './useReferenceArrayInputController';
import { CoreAdminContext } from '../../core';
import { testDataProvider } from '../../dataProvider';
import { SORT_ASC } from '../../reducer/admin/resource/list/queryReducer';
import { SORT_ASC } from '../list/queryReducer';

const ReferenceArrayInputController = props => {
const { children, ...rest } = props;
Expand Down
1 change: 1 addition & 0 deletions packages/ra-core/src/controller/list/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@ export * from './useListFilterContext';
export * from './useListPaginationContext';
export * from './useListParams';
export * from './useListSortContext';
export * from './queryReducer';
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import expect from 'expect';
import queryReducer, { SORT_ASC, SORT_DESC } from './queryReducer';
import { queryReducer, SORT_ASC, SORT_DESC } from './queryReducer';

describe('Query Reducer', () => {
describe('SET_PAGE action', () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { Reducer } from 'redux';
import set from 'lodash/set';

import removeEmpty from '../../../../util/removeEmpty';
import removeKey from '../../../../util/removeKey';
import { ListParams } from '../../../../actions';
import removeEmpty from '../../util/removeEmpty';
import removeKey from '../../util/removeKey';
import { ListParams } from '../../actions';

export const SET_SORT = 'SET_SORT';
export const SORT_ASC = 'ASC';
Expand Down Expand Up @@ -51,7 +51,7 @@ type ActionTypes =
/**
* This reducer is for the react-router query string, NOT for redux.
*/
const queryReducer: Reducer<ListParams> = (
export const queryReducer: Reducer<ListParams> = (
previousState,
action: ActionTypes
) => {
Expand Down
Loading

0 comments on commit ef24549

Please sign in to comment.