Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RFC: configurable produce implementation #3074

Open
wants to merge 28 commits into
base: master
Choose a base branch
from

Conversation

phryneas
Copy link
Member

@phryneas phryneas commented Jan 11, 2023

With immer alternatives like structura.js and mutative popping up, it might make sense to allow the configuration of the produce implementation used by RTK.

This PR would, by default, still use immer, but expose buildCreateReducer and buildCreateSlice functions to create custom createReducer and createSlice functions. This should tree-shake well, and if only those other implementations are used, immer should not be bundled anymore (as soon as the polyfill in 2.0 is disabled, which is why I base this PR against the v2 version).

This would also theoretically allow produce to be swapped out for a stub, so people who really want to use RTK without any immutability helper could do so - even though I would highly recommend against that.

It would also prepare for future situations when Records and Tuples are widely available, and an immutability helper will not be necessary anymore.

There are still a few more places where we use produce, so additional changes would be required in

  • createEntityAdapter
  • createApi (here we need to configure with a version allowing for patches)

So far, I just wanted to put this out as a base for discussion - the code changes required for this are pretty small, so I think it might make sense at this point.

PS: we might need to do some docs updates, as, at the moment, the docs directly reference the types of createSlice and that has now moved into its own interface definition.

PS2: at this point, mutative does not allow for returning a new object, so that one might not work yet. Let's see if we can convince the author to add that functionality.

@codesandbox
Copy link

codesandbox bot commented Jan 11, 2023

CodeSandbox logoCodeSandbox logo  Open in CodeSandbox Web Editor | VS Code | VS Code Insiders

@codesandbox-ci
Copy link

codesandbox-ci bot commented Jan 11, 2023

This pull request is automatically built and testable in CodeSandbox.

To see build info of the built libraries, click here or the icon next to each commit SHA.

Latest deployment of this branch, based on commit 0888fde:

Sandbox Source
Vanilla Configuration
Vanilla Typescript Configuration
rsk-github-issues-example Configuration
@examples-query-react/basic Configuration
@examples-query-react/advanced Configuration
@examples-action-listener/counter Configuration
rtk-esm-cra Configuration

@unadlib
Copy link

unadlib commented Jan 11, 2023

hi @phryneas,

I will add support for return values to mutative. But from the user's point of view, we should suggest always returning the draft itself or no value at all, not only does it maintain optimal performance (no extra performance wasted traversing unpredictable return values), but it also maintains the full mutation feature.

@phryneas
Copy link
Member Author

phryneas commented Jan 11, 2023

Hi @unadlib! That's great news!
In writing reducers with immer (which we might need to rename to "writing mutable immutable reducers" or something like that), we generally recommend changing the existing state - but there are cases where the user might want to return something completely different.

Most common cases would be:

  • replacing the full state with a completely new object (e.g. resetting state or taking a big chunk of data that came in, for example from an api call)
  • the user wants to optimize things by hand
  • they are just migrating over old Redux code that is not doing mutable updates yet.

From our perspective it would be fine if the rule were "if the user returns a value and that value is not the draft, just return that whole new value without traversing and touching it in any way", but I'm not sure if there are additional things that you have to do and cannot skip.

@unadlib
Copy link

unadlib commented Jan 14, 2023

Hi @unadlib! That's great news! In writing reducers with immer (which we might need to rename to "writing mutable immutable reducers" or something like that), we generally recommend changing the existing state - but there are cases where the user might want to return something completely different.

Most common cases would be:

  • replacing the full state with a completely new object (e.g. resetting state or taking a big chunk of data that came in, for example from an api call)
  • the user wants to optimize things by hand
  • they are just migrating over old Redux code that is not doing mutable updates yet.

From our perspective it would be fine if the rule were "if the user returns a value and that value is not the draft, just return that whole new value without traversing and touching it in any way", but I'm not sure if there are additional things that you have to do and cannot skip.

Cool proposal. Since the user may refer to the draft in the return of a completely new object, I will use the option strict, which we will suggest the user enable strict in development mode and disable strict in production mode. This will ensure safe returns and also keep good performance in the production build.

If the return value is mixed drafts or undefined, then use safeReturn() wrapper.

// Ensure the initial state gets frozen either way (if draftable)
let getInitialState: () => S
if (isStateFunction(initialState)) {
getInitialState = () => freezeDraftable(initialState())
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✋ This still relies on Immer imports over in ./utils

packages/toolkit/src/createReducer.ts Show resolved Hide resolved
@markerikson markerikson force-pushed the v2.0-integration branch 2 times, most recently from 606897f to d2d0f70 Compare January 14, 2023 18:18
@unadlib
Copy link

unadlib commented Jan 26, 2023

@phryneas @markerikson Mutative v0.3.2 already supports returning values in the draft functions. It is already fully compliant with produce interface in redux-toolkit and maintains good performance.

@markerikson
Copy link
Collaborator

@unadlib : nice!

One other question that came up is that we use several of Immer's utilities like isDraft and isDraftable, in order to check whether it's safe to pass certain values into produce().

Does mutative have anything like those?

@unadlib
Copy link

unadlib commented Jan 27, 2023

@unadlib : nice!

One other question that came up is that we use several of Immer's utilities like isDraft and isDraftable, in order to check whether it's safe to pass certain values into produce().

Does mutative have anything like those?

@markerikson Yes, Mutative also provides isDraft() and isDraftable().

@markerikson markerikson force-pushed the pr/configure-produce-implementation branch from 0ce2ec0 to cd99bad Compare February 21, 2023 04:38
@markerikson markerikson force-pushed the pr/configure-produce-implementation branch from cd99bad to ef49075 Compare February 21, 2023 04:40
@markerikson markerikson reopened this Feb 21, 2023
@markerikson
Copy link
Collaborator

markerikson commented Feb 21, 2023

I rebased this branch against v2.0-integration.

Also, for kicks, I tried swapping all our usages of Immer to use Mutative instead. Unfortunately, there's several critical test failures, including basic createReducer behavior. Looks like it's hitting a Cannot perform 'has' on a proxy that has been revoked error.

https://github.com/reduxjs/redux-toolkit/tree/feature/try-mutative

@unadlib
Copy link

unadlib commented Feb 21, 2023

I rebased this branch against v2.0-integration.

Also, for kicks, I tried swapping all our usages of Immer to use Mutative instead. Unfortunately, there's several critical test failures, including basic createReducer behavior. Looks like it's hitting a Cannot perform 'has' on a proxy that has been revoked error.

https://github.com/reduxjs/redux-toolkit/tree/feature/try-mutative

hi, @markerikson, I will debug it later. Overall, regarding the integration with Reducer, Immer and Mutative are not exactly equivalent, Mutative supports more options.

@unadlib
Copy link

unadlib commented Feb 21, 2023

I rebased this branch against v2.0-integration.

Also, for kicks, I tried swapping all our usages of Immer to use Mutative instead. Unfortunately, there's several critical test failures, including basic createReducer behavior. Looks like it's hitting a Cannot perform 'has' on a proxy that has been revoked error.

https://github.com/reduxjs/redux-toolkit/tree/feature/try-mutative

For good performance, Mutative does not traverse mixed draft return values by default. When the return value is mixed with drafts, you can use safeReturn().

Mutative docs:

We recommend to enable strict in development mode and disable strict in production mode. This will ensure safe returns and also keep good performance in the production build. If the return value is mixed drafts or undefined, then use safeReturn().

You can fix tests like this,

https://github.com/reduxjs/redux-toolkit/blob/feature/try-mutative/packages/toolkit/src/tests/createReducer.test.ts#L174

- return state.concat({ ...newTodo, completed: false })
+ return safeReturn(state.concat({ ...newTodo, completed: false }))

https://github.com/reduxjs/redux-toolkit/blob/feature/try-mutative/packages/toolkit/src/tests/createReducer.test.ts#L181

- return state.map((todo, i) => {
+ return safeReturn(state.map((todo, i) => {

@markerikson
Copy link
Collaborator

Hmm. That definitely makes it harder to use mutative as a drop-in replacement, because we don't know ahead of time what users are going to do in their reducers.

I think the right answer here is that we'd need to inject a wrapper that uses safeReturn, something like:

const createReducerWithMutative = buildCreateReducer({
  createNextState: (state, recipe) => {
    return create(state, (draft) => {
      return safeReturn(recipe(draft))
    })
  },
  isDraft,
  isDraftable
})

@unadlib
Copy link

unadlib commented Feb 21, 2023

Hmm. That definitely makes it harder to use mutative as a drop-in replacement, because we don't know ahead of time what users are going to do in their reducers.

I think the right answer here is that we'd need to inject a wrapper that uses safeReturn, something like:

const createReducerWithMutative = buildCreateReducer({
  createNextState: (state, recipe) => {
    return create(state, (draft) => {
      return safeReturn(recipe(draft))
    })
  },
  isDraft,
  isDraftable
})

When the user uses createReducerWithImmer, if the reducer wants to return the value undefined, the reducer will have to return return Immer's nothing, and this implicit rule is similar to the case.

I also agree with you. Whether createReducerWithMutative returns undefined explicitly or not is left up to the user, like this.

const createReducerWithMutative = buildCreateReducer({
  createNextState: (state, recipe) => {
    return create(state, (draft) => {
      const value = recipe(draft);
      return value === undefined ? value : safeReturn(value);
    })
  },
  isDraft,
  isDraftable
})

@markerikson markerikson force-pushed the v2.0-integration branch 2 times, most recently from 1348302 to 157536c Compare March 25, 2023 18:58
@unadlib
Copy link

unadlib commented Mar 26, 2023

@markerikson I have refactored the implementation of the return value, once you upgrade to mutative v0.5.0, you will migrate Immer to Mutative smoothly, the relevant tests have passed.

The details are at unadlib/mutative#9

BTW, I tested the Immer v10 branch. Mutative is still more than 15x faster than Immer.

@giusepperaso
Copy link

giusepperaso commented Apr 1, 2023

Hi, I'm the author of structurajs. Thanks for this pull request!

After some work I think I finally managed to make the library fully compatible with redux toolkit

As an example I did setup a try-structura branch which passes all of the tests contained into the packages/toolkit folder

Note that in this example branch I removed Immer completely, and I put all of my exports inside the exports.ts file

Also, I called enableAutoFreeze and enableStandardPatches to make all of the tests pass, but they are not always necessary and could be disabled to gain some performance:

  • structura does not allow the produced state to be modified (at compile time via typescript), so relying on object freeze could be a bit redundant
  • structura by default does not use standard json patches, but a proprietary format based on linked lists which is slightly faster; you probably don't need standard json patches unless you have to manipulate them with another library

If you see any problem or if you need help with something let me know! @phryneas @markerikson

@markerikson
Copy link
Collaborator

@giusepperaso : thanks for working on that!

FWIW we really do want freezing in place, because we want users to see errors at runtime if they attempt to mutate outside of a reducer. (Not everyone uses TS!)

@EskiMojo14
Copy link
Collaborator

is it worth making a configurable version of createDraftSafeSelector too? that currently uses immer's isDraft() and current()

@markerikson markerikson added this to the Post 2.0 milestone Oct 1, 2023
@netlify
Copy link

netlify bot commented Oct 2, 2023

Deploy Preview for redux-starter-kit-docs ready!

Name Link
🔨 Latest commit 0888fde
🔍 Latest deploy log https://app.netlify.com/sites/redux-starter-kit-docs/deploys/651b2b31e7e2f100084383b4
😎 Deploy Preview https://deploy-preview-3074--redux-starter-kit-docs.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify site configuration.

@markerikson
Copy link
Collaborator

Found another similar lib: https://github.com/tnfe/limu

Base automatically changed from v2.0-integration to master December 4, 2023 04:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants