ParcelBoundary

ParcelBoundary is a React component. Its job is to optimise rendering performance, and to optionally control the flow of parcel changes.

Each ParcelBoundary is passed a Parcel. By default the ParcelBoundary uses pure rendering, and will only update when the Parcel’s data changes to avoid unnecessary re-rendering.

ParcelBoundaries have an internal action buffer that can hold onto changes as they exit the boundary. These are normally released immediately, but also allow for debouncing changes, or putting a hold on all changes so they can be released later.

import ParcelBoundary from 'react-dataparcels/ParcelBoundary';
<ParcelBoundary
    parcel={Parcel}
    debounce={?number}
    pure={?boolean}
    forceUpdate={?Array<any>}
    hold={?boolean}
    modifyBeforeUpdate={?Array<Function>}
    onCancel={?Array<Function>}
    onRelease={?Array<Function>}
    debugBuffer={?boolean}
    debugParcel={?boolean}
>
    {(parcel, control) => Node}
</ParcelBoundary>
ParcelBoundary is also available as a React higher order component, ParcelBoundaryHoc.

Children

childRenderer

(parcel: Parcel, control: ParcelBoundaryControl) => Node

type ParcelBoundaryControl = {
    release: () => void,
    cancel: () => void,
    buffered: boolean,
    buffer: Action[]
}

ParcelBoundaries must be given a childRenderer function as children. This is called whenever the ParcelBoundary updates.

It is passed a parcel and a ParcelBoundaryControl instance.

  • The parcel is on the “inside” of the parcel boundary, and is able to update independently of the parcel that was passed into the ParcelBoundary.
  • The control argument passes a ParcelBoundaryControl which can be used to control the ParcelBoundary’s action buffer (see the hold example) and information about the current state of the action buffer.

ParcelBoundaryControl

  • The release() function will release any changes in the buffer, allowing them to propagate upward out of the ParcelBoundary.
  • The cancel() function will cancel any changes in the buffer.
  • The buffered boolean indicates if the ParcelBoundary currently contains changes that it hasn’t yet released.
  • The buffer array contains the actions that are currently held in the buffer.
  • originalParcel contains the Parcel that was passed into the ParcelBoundary, unaffected by any buffering or ParcelBoundary state.

The return value of childRenderer will be rendered.

// personParcel is a Parcel
<ParcelBoundary parcel={personParcel}>
    {(parcel, control) => {
        return <input type="text" {...parcel.spreadDOM()} />;
    }}
</ParcelBoundary>

Props

parcel

parcel: Parcel

The parcel that the ParcelBoundary will apply to. By default the ParcelBoundary will only update when parcel's data changes.

The parcel can be accessed from inside the ParcelBoundary via the first argument of the child renderer function, as shown here.

// personParcel is a Parcel
<ParcelBoundary parcel={personParcel}>
    {(personParcel) => {
        // personParcel is now inside the ParcelBoundary
        return <input type="text" {...personParcel.spreadDOM()} />;
    }}
</ParcelBoundary>

debounce

debounce?: number // optional

If set, debounce will debounce any changes that occur inside the ParcelBoundary. The number indicates the number of milliseconds to debounce.

This can be used to increase rendering performance for parcels that change value many times in rapid succession, such as text inputs.

Debouncing explained

When the parcel in the ParcelBoundary sends a change, the ParcelBoundary will catch it and prevent it from being propagated out of the boundary. The parcel on the inside of the ParcelBoundary will still update as normal.

The ParcelBoundary waits until no new changes have occured for debounce number of milliseconds. It then releases all the changes it has buffered, all together in a single change request.

Debouncing can be good for rendering performance because parcels outside the ParcelBoundary don’t needlessly update every time a small change occurs (e.g. each time the user presses a key).

// personParcel is a Parcel
<ParcelBoundary parcel={personParcel} debounce={100}>
    {(personParcel) => <input type="text" {...personParcel.spreadDOM()} />}
</ParcelBoundary>

See an example of ParcelBoundary debounce

pure

pure?: boolean = true // optional

Enables pure rendering. When pure is true, ParcelBoundary will only re-render when parcel's data changes. It defaults to true.

Use forceUpdate if you would like ParcelBoundary to re-render in response to changes in other props.

See an example of ParcelBoundary pure

forceUpdate

forceUpdate?: Array<*> // optional

While a ParcelBoundary is using pure rendering, forceUpdate will force the ParcelBoundary to re-render in response to changes in other props. Each item in the forceUpdate array is compared using strict equality against its previous values, and if any are not strictly equal, the ParcelBoundary will re-render.

// personParcel is a Parcel
// options is an array of options that are loaded after mount

<ParcelBoundary parcel={personParcel} forceUpdate={[options]}>
    {(personParcel) => <Select {...personParcel.spreadDOM()} options={options} />}
</ParcelBoundary>
Example

hold

hold?: boolean = false // optional

When hold is true, all changes made to the parcel inside the ParcelBoundary are prevented from being propagated out of the boundary. The inner parcel will continue to update as normal. You can then call control.release() to release all the buffered changes at once, or control.cancel() to cancel all the buffered changes. This can be useful for building UIs that have a submit action.

// personParcel is a Parcel
<ParcelBoundary parcel={personParcel}>
    {(personParcel, {release, cancel}) => {
        // personParcel is now inside the ParcelBoundary
        return <div>
            <input type="text" {...personParcel.spreadDOM()} />
            <button onClick={() => release()}>Submit</button>
            <button onClick={() => cancel()}>Cancel</button>
        </div>;
    }}
</ParcelBoundary>
Example

modifyBeforeUpdate

modifyBeforeUpdate?: Array<Updater>

// updating value - only to be used if shape doesn't change
type Updater = (value: any, changeRequest: ChangeRequest) => any;

// updating shape, including meta
type Updater = shape((parcelShape: ParcelShape, changeRequest: ChangeRequest) => any);

The modifyBeforeUpdate prop allows derived data to be set on the Parcel in the ParcelBoundary. Whenever the data in a ParcelBoundary is about to be initialised or updated in any way, it is passed through all modifyBeforeUpdate functions.

Each function in the modifyBeforeUpdate array operates just like the updater provided to Parcel.modifyUp().


let modifyBeforeUpdate = [
    (value) => ({
        ...value,
        isLong: value.word.length > 15
    })
];

// exampleParcel is a Parcel containing
// {
//    word: "cool",
//    isLong: undefined
//    // ^ this will contain derived data
// }

<ParcelBoundary parcel={exampleParcel} modifyBeforeUpdate={modifyBeforeUpdate}>
    {(exampleParcel) => <input type="text" {...exampleParcel.get('word').spreadDOM()} />}
</ParcelBoundary>

// exampleParcel.value.isLong will always return true if word is longer than 15

Please be careful

This method is safe to use without shape() in most cases, but in some cases it should not be used:

  • If the updater gives you a primitive value or childless value, you can return anything.
  • If the updater gives you a value that has children, you can always return a primitive value or childless value.
  • If the updater gives you a value that has children, you can return a value with children only if the shape hasn’t changed.

Please ensure you do not change the shape of the value, as changing the data shape or moving children within the data shape can cause dataparcels to misplace its keying and meta information! Dataparcels stores data against each part of a deep value’s data structure, so it can only let you change the value if you promise not to alter the shape.

// example updaters
string => string + "!" // good
string => [string] // good
date => date.toString() // good
array => array.join(".") // good
array => array.map(number => number + 1) // good, shape is still the same

array => array.slice(0,2) // bad, shape has changed if array is longer that 2!
array => array.reverse() // bad, shape has changed because items have moved around!

If you need to update the shape of the data, you can do so using dataparcels/shape. The shape function will wrap your Parcel’s data in a ParcelShape which allows for safe shape editing. See ParcelShape for more details.

import shape from 'dataparcels/shape';

let parcel = new Parcel({
    value: [1,2,3]
});

let modifiedParcel = parcel.modifyDown(shape(parcelShape => parcelShape
    .push("foo")
    .push("bar")
    .setMeta({
        cool: true
    })
));

keepValue

keepValue?: boolean = false // optional

The default behaviour of ParcelBoundary is to update its contents whenever it receives a new Parcel via props. This behaviour is preferred in nearly all use cases.

If keepValue is true on a ParcelBoundary, and if that ParcelBoundary propagates a change, then that ParcelBoundary will not update its value when it receives a new Parcel via props.

This is useful when modify methods are being used above the ParcelBoundary, which change the data type on it’s trip up to the top level Parcel and back down again.

let numberParcel = parcel
    .modifyDown(number => `${number}`)
    .modifyUp(string => Number(string));

return <ParcelBoundary parcel={numberParcel} keepValue>
    {(parcel) => <input type="text" {...parcel.spreadDOM()} />}
</ParcelBoundary>;

// without keepValue, if you type "0.10" in the input above it would
// immediately be replaced with "0.1", as the new value is turned
// into a number on the way up, and into a string on the way down,
// which would make typing very frustrating.

// keepValue keeps "0.10" in the text field.
Example

onCancel

onCancel?: Array<ContinueChainFunction> // optional

type ContinueChainFunction = (continueCancel: Function) => void

The onCancel function array can be used to add behaviour before or after a ParcelBoundary cancels the changes in its buffer.

If onCancel is provided and ParcelBoundaryControl.cancel() is called, the buffer’s changes are not immediately cancelled, and the first element of onCancel is called instead. The onCancel function is passed a continueCancel() function as an argument, and if this continueCancel() function is called then it will call the next element of the onCancel array. If continueCancel() is called on the last element in the onCancel array, then it will cancel the changes in the buffer.

// add a delay to the cancel action
let onCancel = continueCancel => setTimeout(continueCancel, 1000);
<ParcelBoundary parcel={parcel} onCancel={[onCancel]}>
// ...

onRelease

onRelease?: Array<ContinueChainFunction> // optional

type ContinueChainFunction = (continueRelease: Function) => void

The onRelease function array can be used to add behaviour before or after a ParcelBoundary releases the changes in its buffer.

If onRelease is provided and ParcelBoundaryControl.release() is called, the buffer’s changes are not immediately released, and the first element of onRelease is called instead. The onRelease function is passed a continueRelease() function as an argument, and if this continueRelease() function is called then it will call the next element of the onRelease array. If continueRelease() is called on the last element in the onRelease array, then it will release the changes in the buffer.

// add a delay to the release action
let onRelease = continueRelease => setTimeout(continueRelease, 1000);
<ParcelBoundary parcel={parcel} onRelease={[onRelease]}>
// ...

debugBuffer

debugBuffer?: boolean = false // optional

Wehn debugBuffer is true, ParcelBoundary will log out changes relating to its internal action buffer.

debugParcel

debugParcel?: boolean = false // optional

Wehn debugParcel is true, ParcelBoundary will log out changes to its current parcel state.