useParcelState

The useParcelState function is a React hook. Its job is to provide you with a Parcel stored in React state, and to handle how the parcel responds to changes in React props.

By default, useParcelState operates in a similar way to an uncontrolled component, accepting an initial value once on first mount, and then becoming the master of its own state from then on. This behaviour can be changed using the updateValue parameter.

import useParcelState from 'react-dataparcels/useParcelState';
let [parcel] = useParcelState({
    value: any,
    // optional
    updateValue?: boolean,
    rebase?: boolean,
    onChange?: Function,
    onChangeUseResult?: boolean,
    beforeChange?: Function|Function[]
});
If you want a submittable form, or if you'd like to debounce your onChange function, consider using useParcelForm.

Params

value

value: any

Sets the initial value to be put into useParcelState's Parcel.

let [parcel] = useParcelState({
    value: 100
});

// parcel.value is 100

parcel.set(200);

// set() triggers a change and a re-render
// parcel.value is now 200

If computing value is a heavy operation, you can return the value from a function. The function will only be called on initial mount, and is passed the previous value. However, if updateValue is set to true then the function will be called on every update.

let newValue = 100;

let [parcel] = useParcelState({
    value: (prevValue) => newValue
});

// parcel.value is 100

Value can also accept parcel updaters. These pass the previous data held in state, and expect the new data to be returned. These can be useful for setting parcel meta.

import asNode from 'react-dataparcels/asNode';

let [parcel] = useParcelState({
    value: asNode(node => node
        .update(() => newValue)
        .setMeta({
            foo: true
        })
    )
});

Returning promises from value

import asyncValue from 'react-dataparcels/asyncValue';

let [parcel] = useParcelState({
    value: asyncValue(async () => {
        // add logic here
        return theValue;
    })
});

It's possible to return a value from a promise using the asyncValue function. The useParcelState hook's parcel has a value of undefined until the promise resolves. Once it has resolved, useParcelState hook's parcel has a value of the result of the async function.

When using asyncValue you will also receive its status via valueStatus.

updateValue

updateValue?: boolean = false // optional

When updateValue is set to true during an update, the useParcelState hook will check to see if value has changed, and will update its Parcel's value if so. This will completely replace any changes that may have happened to the Parcel since the last time value was put into the Parcel.

Note that it will also cause any downstream useParcelBuffer hooks and ParcelBoundaries to forget all their buffered changes, unless rebase is used.

Value changes are detected using Object.is(), comparing the new value with the previous one.

// receivedValue is 100

let [parcel] = useParcelState({
    value: receivedValue,
    updateValue: true
});

// parcel.value is 100

parcel.set(200);

// set() triggers a change and a re-render
// parcel.value is now 200

// if component updates and receivedValue is now 300
// then parcel.value is now 300

rebase

rebase?: boolean = false // optional

As described above, updates caused by updateValue will cause any downstream useParcelBuffer hooks and ParcelBoundaries to forget all their buffered changes. This is safe default behaviour because changes in the downstream buffers may not be compatible with the new Parcel's data shape. However it may be user unfriendly in some cases, depending on when and how often the parcel updates.

Setting rebase to true will prevent downstream buffers from being cleared.

Please note

Only use this if the shape of your data does not change, so that downstream buffered changes are compatible with the new Parcel's data shape.

This restriction will be lifted in future with the introduction of a feature known as rekey.

onChange

onChange?: (parcel: Parcel, changeRequest: ChangeRequest) => any // optional

If provided, this function is called whenever useParcelState's Parcel has handled a change. It receives the new Parcel, and the ChangeRequest that was responsible for the change.

This function can be used to relay changes further up the React heirarchy.

Please keep in mind that it is possible for a change to result in the same data being contained in the Parcel, onChange will not dedupe subsequent calls whose Parcels contain the same data. ChangeRequest.hasValueChanged() can help with that.

let [parcel] = useParcelState({
    value,
    onChange: (parcel, changeRequest) => {
        // add logic here
    }
});

Returning promises from onChange

import asyncChange from 'react-dataparcels/asyncChange';

let [parcel] = useParcelState({
    value,
    onChange: asyncChange(async (parcel, changeRequest) => {
        // add logic here
    })
});

It's possible to return a promise from onChange using the asyncChange function. When doing this, the change does not enter the hook's state until the promise resolves.

If another change arrives while a promise is still pending, it will be passed through onChange after the first promise is resolved or rejected. This is to ensure that there is only one operation happening at a time. If the first ChangeRequest's promise is rejected, the changes will be merged with the next ChangeRequest when onChange is called the second time.

This is discussed in more detail in data synchronisation.

When using asyncChange you will also receive its status via changeStatus.

onChangeUseResult

onChangeUseResult?: boolean = false // optional

When true, this sets the value of the hook's parcel to the return value of onChange.

let [parcel] = useParcelState({
    value: receivedValue,
    onChange: (parcel, changeRequest) => {
        return saveMyData(parcel.value);
        // ^ saveMyData send a request to a server to save the data,
        // and returns a promise containing the updated data from the server
    },
    onChangeUseResult: true
});

Note that it will also cause any downstream useParcelBuffer hooks and ParcelBoundaries to forget all their buffered changes, unless rebase is used.

beforeChange

beforeChange?: ParcelUpdater|ParcelUpdater[] // optional

type ParcelUpdater = (value: any, changeRequest: ChangeRequest) => any
type ParcelUpdater = asNode((node: ParcelNode, changeRequest: ChangeRequest) => ParcelNode);
type ParcelUpdater = asChildNodes((nodes: any, changeRequest: ChangeRequest) => any);

The beforeChange parameter accepts either a single function, or an array of functions. Whenever a new value is taken into useParcelState from params, and whenever the useParcelState hook recieves a change from below, the change is passed through each beforeChange function.

Internally the useParcelBuffer hook uses Parcel.modifyUp() on each of the beforeChange functions. If more than one function is passed to beforeChange, the change will go through the first function in the array first, then the second etc.

This is particularly useful for setting derived data, and plugins such as validation are built to be passed into beforeChange.

Please be careful

This method is safe to use in most simple 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.

To find out why, and what to do about it, please read about parcel updaters.

let [parcel] = useParcelState({
    value: "ABC",
    beforeChange: value => value.toLowerCase()
});

// ^ "ABC" will be passed through `beforeChange`
// and useParcelState's Parcel will contain a value of "abc"
// parcel.value is now "abc"

parcel.set("HELLO");

// ^ "HELLO" will be passed through `beforeChange`
// and useParcelState's Parcel will contain a value of "hello"
// parcel.value is now "hello"

Returns

[parcel: Parcel, parcelControl: ParcelHookControl]

parcel

parcel: Parcel

The first element of the returned array is the parcel that this hook holds in React state.

parcelControl

parcelControl: ParcelHookControl

The second element of the returned array is an object containing data and functions for controlling the hook.

ParcelHookControl

type ParcelHookControl {
    valueStatus?: Object,
    changeStatus?: Object
}
  • valueStatus ?: Object
    This object will exist if `value` is passed an `asyncValue` function. It's an object containing information about the current state of the execution of the async `value` function. This is useful if you want to conditionally render elements based on the state of the promise.
    • status : string

      Status is always one of three possible string values:

      • "pending" - if asyncValues promise is pending.
      • "resolved" - if the last promise returned from asyncValue was resolved.
      • "rejected" - if the last promise returned from asyncValue was rejected.
    • isPending : boolean

      The isPending boolean is true if asyncValues promise is pending, otherwise it is false.

    • isResolved : boolean

      The isResolved boolean is true if the last promise returned from asyncValue was resolved.

    • isRejected : boolean

      The isRejected boolean is true if the last promise returned from asyncValue was rejected.

    • error : any

      If the last promise returned from asyncValue was rejected, this contains the rejected promise's payload.

  • changeStatus ?: Object
    This object will exist if `onChange` is passed an `asyncChange` function. It's an object containing information about the current state of the execution of the async `onChange` function. This is useful if you want to conditionally render elements based on the state of the promise.
    • status : string

      Status is always one of four possible string values:

      • "idle" - no promises have yet been returned from asyncChange
      • "pending" - if asyncChanges promise is pending.
      • "resolved" - if the last promise returned from asyncChange was resolved.
      • "rejected" - if the last promise returned from asyncChange was rejected.
    • isPending : boolean

      The isPending boolean is true if asyncChanges promise is pending, otherwise it is false.

    • isResolved : boolean

      The isResolved boolean is true if the last promise returned from asyncChange was resolved.

    • isRejected : boolean

      The isRejected boolean is true if the last promise returned from asyncChange was rejected.

    • error : any

      If the last promise returned from asyncChange was rejected, this contains the rejected promise's payload.