@wrap-mutant/react
v0.0.3
Published
Allow object mutation and changing its reference without recreating
Downloads
8
Maintainers
Readme
Wrap mutant. React
Object mutation is easy and extremelly fast. But such libraries like react make us to rebuild objects on every their change. It's not a problem on simple and small objects. When your object is a big array, your application become slow. When you are trying to handle complicated deeply nested object, it becomes a brain cancer.
Solution is in wrapping that big or complex objects into Proxy object.
Examples
reactflow example [demo | repo]
pravosleva's substring-highlight-sample [demo | repo]
This package contains react integration. To understand how actually it works please read the docs of @wrap-mutant/core API V2. Don't be afraid, it's small.
useWMState
Classical example. We have avoided rebuilding on each render potencially large array. State update complexity does not depends on array size and always happens by O(1)
import React, { useCallback, useEffect } from "react";
import { useWMState } from "@wrap-mutant/react";
const recordFactory = () => [] as string[];
export const Blackboard = () => {
const [records, updateRecords] = useWMState(recordFactory, { bind: true });
const writeRecord = useCallback(() => {
records.push("I will not skateboard in the halls.");
updateRecords();
}, [records, updateRecords]);
useEffect(() => {
const interval = setInterval(writeRecord, 250);
return () => clearInterval(interval); // eslint-disable-next-line
}, []);
const renderedRecords = records.map((item, index) => (
<div className="line" key={index}>
{item}
</div>
));
return <div className="board">{renderedRecords}</div>;
};
It's possible to avoid all loops in this component via pushing into records array rendered JSX.Element
instead of string. But keep in mind it's dirty hack. Of course, we will talk about it next at createMutableContext and @wrap-mutant/react-rendered-array :)
API reference:
- Required factory
function
, passed directly useMemo - Optional options:
object
:deps
: Optional dependencyArray
, passed directly useMemo. Default:[]
bind
: Optionalboolean
flag should we call utilitybindCallables
defined at @wrap-mutant/util. Default:false
. Read more explaination in Pitfalls sectionargs
: Optionalany
generic parameter passed into factoryfunction
(first parameter). Allows you to move complicated factory functions outside yourFunctionalComponent
closure to improve your code readability and performancewrap
: Optionalboolean
meaning should we wrap the target object or not. Default:true
count
: Optionalnumber
parameter meaning how many wrapper objects will be pre-created. More info at @wrap-mutant/core API V2
createMutableContext
Now I imagine you say "WAAAT?", but I'll explain :). This is auxiliary tool created for @wrap-mutant/react-rendered-array-like objects. And if you think it's useless -- start from reading about @wrap-mutant/react-rendered-array, and then welcome here.
In very short words MutableContext is the way to keep actual callbacks without element re-rendering. This is the only way to pass new callbacks into @wrap-mutant/react-rendered-array array-like objects without their's re-render.
Be really careful in MutableContext
usage. Be sure you understand how actually react
works and why does render triggers.
Usage is absolutelly the same as regular context. Limitations:
- You have to pass
Object
-like value anyway even you have the only callback - NEVER unpack this context. Read How do JavaScript closures work?
import { createMutableContext } from "@wrap-mutant/react";
const ReviewsItemCTX = createMutableContext({ updateItem: (diff: any) => {} });
const ItemRender = (props: Item) => {
const ctx = useContext(ReviewsItemCTX); // <= DO NOT UNPACK
return (
<ReviewsItem item={props} updateItem={(diff) => ctx.updateItem(diff)} />
// ALSO WRONG updateItem={ctx.updateItem}
);
};
const Container = () => {
// ... All code skipped. You can see more at examples
// prettier-ignore
const updateItem = useCallback(
(diff: any) => {/* do update state */},
[/* requirements. Everything as usual */],
);
return (
// Again. Context value have to be Object-like
<ReviewsItemCTX.Provider value={{ updateItem }}>
{/* children */}
</ReviewsItemCTX.Provider>
);
};
All these weird things are created to make possible implementation for @wrap-mutant/react-rendered-array-like objects
re-exports
from @wrap-mutant/core API V2:
import { wrap, toggle, HasWrapperGen } from "@wrap-mutant/react";
- function
wrap
is renamed export of @wrap-mutant/core'swrapCached
- function
toggle
is renamed export of @wrap-mutant/core'stoggleCached
- type
HasWrapperGen
is @wrap-mutant/core'sHasWrapperGen
from @wrap-mutant/utils:
import { bindCallables } from "@wrap-mutant/react";
- function
bindCallables
is @wrap-mutant/utils'sbindCallables
Pitfalls
Wrapped target object's methods behavior changes by Proxy object -- they loose their's this
. There is an example:
import { wrap } from "@wrap-mutant/react";
const A = wrap([] as number[]);
A.push(1, 2, 3, 4, 5); // <== throws an Error
A.forEach(concole.log); // <== throws an Error too
In this example push
and forEach
methods lost their's this
. More commonly used map
method also loose his this
. Solution:
import { wrap, bindCallables } from "@wrap-mutant/react";
const A = wrap(bindCallables([] as number[]));
A.push(1, 2, 3, 4, 5); // <== OK
A.forEach(concole.log); // <== OK
It means before wrap
ping you have to apply bindCallables
to target object. And exactly this is a meaning of bind
option of useWMState hook.
General rule sounds like:
If you are calling methods of
wrap
ped object and you are sure these methods implementation is not an arrow function, you have tobind callables
beforewrap
ping.
Any questions?
Don't be afraid to open this library source code -- it's really small. Also we have Telegram Community