npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

totally-not-mutable

v1.0.25

Published

This one trick makes all your JS mutability and undo/redo problems disappear (doctors hate it!).

Downloads

28

Readme

TotallyNotMutable (BETA)

This one trick makes all your JS mutability and undo/redo problems disappear (doctors hate it!).

This is in beta and should not be considered stable until version 2.0. Please stay tuned.

Intro

This package contains two vanilla JS classes that use structural sharing to accomplish immutability and versioning (undo/redo). Compatible with plain objects, Dates, Maps, and Sets. Integrates easily into major frameworks (react, angular, etc.). Optimized for mutations over time.

TotallyNotMutable

A class built to handle modifications to an object without mutating the original object.

//step 1 - set the value
const intialData = {foo:'bar',arr:['hello','world'], obj:{a:'a',anotherObject:{bool:true}}};
const tnm = new TotallyNotMutable<typeof intialData>({autoFreeze:false}); //configuration is optional
tnm.setValue(initialData);

//step 2 - mutate
const step2Value = tnm.mutate((value)=>{
    value.foo = 'bar no more!';
    value.arr.push('again');
    value.obj.a = 'a has been updated';
});
const structuralSharing = intialData.obj.anotherObject === step2Value.obj.anotherObject; // true

//step 3 - ????

//step 4 - PROFIT!!

TotallyVersionable

A class built to handle undo/redo. Built on top of TotallyNotMutable

const intialData = {foo:'bar',arr:['hello','world'], obj:{a:'a',anotherObject:{bool:true}}};
const tnm = new TotallyVersionable<typeof intialData>({autoFreeze:false}); //configuration is optional
//step 1 - set the value
tnm.setValue(initialData);

//step 2 - mutate
const secondValue = tnm.mutate((value)=>{
    value.foo = 'bar no more!';
    value.arr.push('again');
    value.obj.a = 'a has been updated';
});

//step 3 - undo
const thirdValue = tnm.undo();
const isEqualToInitial = intialData === thirdValue; //true

//step 4 - redo
const fourthValue = tnm.redo();
const isEqualToMutated = secondValue === fourthValue; //true

Confguration

Both classes' constructors take a single parameter as the configuration which can enable auto freezing. This will be a performance hit but can be usefull in a development environment to ensure resulting data is not being modified externally.

const tnm = new TotallyNotMutable<SomeType>({autoFreeze:true})

Immer vs ImmutableJS vs TotallyNotMutable - Fight!

Each library has its benefits and drawbacks.

TotallyNotMutable vs Immer

One disadvantage to using Immer's produce(input, (draft => {})) method is that the input data needs to be fully processed (proxied) from scratch before each mutation unlike TotallyNotMutable, which is optimized for mutations. TotallyNotMutable processes the input data up front once, then with each mutation, only the parts that have changed need to be processed again. However, to achieve faster writes, the proxy stays in memory when using TotallyNotMutable.

TotallyNotMutable vs ImmutableJS

One disadvantage to ImmutableJS is that developers have to learn custom objects and API methodss. TotallyNotMutable's mutate() method allows developers to use standard javascript. Read/write performance is similar.

Performance Metrics

Tests run on Apple Macbook Pro, 32 GB ram, M1 Pro processor

| Test | TotallyNotMutable | Immer | ImmutableJS | Notes | | --------------------------------- | ----------------- | ------ | ----------- | ------------------------------------------------------------------ | | 1k mutation calls with one update | 4ms | 1962ms | 5ms | 1k calls to immer's produce(), ImmutableJS includes a toJs() call, | | 1 mutation call with 1k updates | 2ms | 2ms | 2ms | one call to immer's produce(), ImmutableJS uses withMutations() , | | Read 700kb | 5ms | 0ms | 7ms | |

Integration with React

TotallyNotMutable and Totally versionable are easily integrated into React. The following hooks can be used in any component or can be part of a context provider.

// TotallyNotMutable hook
export function useTotallyNotMutable<T>() {
  const totallyNotMutableRef = useRef(new TotallyNotMutable<T>());

  const [value, setValue] = useState<T>();

  const mutate = useCallback(
    (handler: (value: T) => void) => {
      setValue(totallyNotMutableRef.current.mutate(handler));
    },
    [setValue]
  );

  const setValueCallback = useCallback(
    (value: T) => {
      setValue(totallyNotMutableRef.current.setValue(value));
    },
    [setValue]
  );

  return {
    value,
    mutate,
    setValue: setValueCallback,
  };
}

//TotallyVersionable hook
export function useTotallyVersionable<T>() {
  const versionHandlerRef = useRef(new TotallyVersionable<T>());

  const [value, setValue] = useState<T>();

  const mutate = useCallback(
    (handler: (value: T) => void) => {
      setValue(versionHandlerRef.current.mutate(handler));
    },
    [setValue]
  );

  const addVersion = useCallback(
    (value: T) => {
      setValue(versionHandlerRef.current.pushVersion(value));
    },
    [setValue]
  );

  const undo = useCallback(() => {
    setValue(versionHandlerRef.current.undo());
  }, [setValue]);

  const redo = useCallback(() => {
    setValue(versionHandlerRef.current.redo());
  }, [setValue]);

  const getSizes = useCallback(() => {
    return versionHandlerRef.current.getSizes();
  }, []);

  const getVersion = useCallback(
    (index: number) => versionHandlerRef.current.getVersion(index),
    []
  );

  const getEvents = useCallback(
    () => versionHandlerRef.current.getEvents(),
    []
  );

  const getVersions = useCallback(
    () => versionHandlerRef.current.getVersions(),
    []
  );

  return {
    value,
    mutate,
    addVersion,
    undo,
    redo,
    getSizes,
    getVersion,
    getEvents,
    getVersions,
  };
}


//In a context provider
export type TotallyNotMutableContext<T> = ReturnType<typeof useTotallyNotMutable<T>>;

export const GlobalContext = React.createContext<TotallyNotMutableContext<MyType>>(
  {} as TotallyNotMutableContext<MyType>
);

function SomeComponent(){
    const { value, mutate, setValue } = useContext(GlobalContext);

    return <div></div>
}

function App() {
  const versionableCtx = useTotallyNotMutable<MyType>();

  return (
    <GlobalContext.Provider value={versionableCtx}>
      <div className="App">
        <SomeComponent>
      </div>
    </GlobalContext.Provider>
  );
}

export default App;

FAQ

Why was TotallyNotMutable/TotallyVersionable written?

Once upon a time, I needed to implemented undo/redo functionality. ImmutableJS seemed cool, so I used it. It worked well. I was happy with it. I didn't touch the code for over a year. Then one day, I had to make some updates and realized having to relearn the ImmutableJS API again was a pain. At some point during all this, I read some comment from a seemingly opinionated developer that said something to the effect of, "You don't need ImmutableJS, you can just use the spread operator". This comment stuck with me for a while. I wanted to write something that automatically used the spread operator but didn't need to re-process the input for each mutation like ImmerJS.

When do I use TotallyNotMutable vs TotallyVersionable?

Use TotallyVersionable when you need undo/redo. If you don't need undo/redo, you likely should be fine with just TotallyNotMutable.

What's up with the package name? Why does the name have "not mutable"? You know it's "immutable" right?

Yep. "Immutable" was already taken. TotallyNotMutable's a dope name.