Skip to content
This repository has been archived by the owner on Feb 14, 2023. It is now read-only.

Support property-specific callbacks. #148

Open
m4ttheweric opened this issue Jan 23, 2020 · 6 comments
Open

Support property-specific callbacks. #148

m4ttheweric opened this issue Jan 23, 2020 · 6 comments
Labels
feature A new feature is requested.

Comments

@m4ttheweric
Copy link

m4ttheweric commented Jan 23, 2020

Hi there,

I am looking for a way for code that is not within a component to subscribe to change to state, akin to an rxjs Subject. I have some api code that needs to make use of a Provider state that has the current path to the host, and if the path changes, it needs to be able to respond accordingly.

Currently, addCallback is my only option, but I have to set up manual checking for if the state property I am concerned about changes, and it feels a bit clunky and unnecessary to be executing on every state change.

Am I missing any current capabilities that would work for this situation? Perhaps something in addCallback that gives detail on what state changed?

Thanks!

Matt

@m4ttheweric
Copy link
Author

m4ttheweric commented Jan 23, 2020

FYI, this is a sketch of a utility method for subscribing to reactn state properties assuming the use of providers. It uses rxjs to subscribe and underscore for evaluation of equality.

import ReactNProvider from 'reactn/types/provider';
import { BehaviorSubject } from 'rxjs';
import { isEqual } from 'underscore';

function Subscribe<SubjectType>(
   provider: ReactNProvider,
   stateProperty: string
): BehaviorSubject<SubjectType> {
   //set initial value
   let stateSubject = new BehaviorSubject<SubjectType>(
      provider.getGlobal()[stateProperty]
   );

   //add a callback to the provider to update the subject
   provider.addCallback(state => {
      if (!isEqual(stateSubject.getValue(), state[stateProperty])) {
         stateSubject.next(state[stateProperty]);
      }
   });

   return stateSubject;
}

Then, in a utility method/helper code, you can subscribe to a reactn state value by doing something like:

const user = Subscribe<IUser>(AppState, 'user');
user.subscribe(u => { console.log('Your user has changed!') });

@quisido
Copy link
Collaborator

quisido commented Jan 25, 2020

Thank you for the great idea. addCallback is your best bet. It does not currently support the functionality to subscribe a single property.

This middleware between the callback and your utility may help:

function createPropertyCallback(property, callback) {
  let lastValue;
  return (globalState) => {
    const newValue = globalState[property];
    if (newValue !== lastValue) {
      lastValue = newValue;
      callback(newValue);
    }
  };
}

addCallback(
  createPropertyCallback(
    'user',
    function(user) {
      console.log(user);
    }
  )
);

I wrote it free-hand, so sorry for any bugs, but it should be enough to get you started.

I'd like to see addCallback(callback, 'property') or some variant, but it's not priority enough on my schedule since it can be unblocked with the above. I welcome anyone who wants to contribute something like that, though.

@quisido quisido changed the title Provide a way for a library/helper function to subscribe to state changes, akin to useGlobal in components Support property-specific callbacks. Jan 25, 2020
@quisido quisido added the feature A new feature is requested. label Jan 25, 2020
@m4ttheweric
Copy link
Author

@CharlesStover Thanks for the reply! My only concern about the middle-ware solution you provided is the equality statement you wrote. Were you only intending it for simple data-types like string/numbers? I don't think newValue !== lastValue would work for arrays/objects or other types. But perhaps your intention was to provide a sketch? Thanks again!

@quisido
Copy link
Collaborator

quisido commented Jan 28, 2020

The only time this would be a problem is if you copied the object/array but did not change any values.

A reference of an object to itself is considered equal.

const a = [1, 2, 3];
const b = a;
a === b; // true

In ReactN, these values will be the same each render. The only time they would change is if you change them, but set them to the same value:

const a = [1, 2, 3];
const b = [...a];
a === b; // false

The second example is false because B is a new array that just copies A.

If A is your global state array, it should not change between renders. It would only "change" if you did something like this:

setGlobal({
  a: [...global.a],
});

Hope this helps.

There are a lot of utility libraries out there for doing shallow and deep comparisons of objects and arrays. These may be useful for handling your edge-cases.

@m4ttheweric
Copy link
Author

m4ttheweric commented Jan 28, 2020

Thanks so much for your middleware idea, I was able to come up with a utility function based on that and I was able to remove the need for rx-js. The key difference is that my function allows you to specify as many or as few properties as you want to listen to:

import ReactNProvider from 'reactn/types/provider';
import { isEqual } from 'underscore';

type IPropertyConfig<StateType> = {
   [P in keyof StateType]?: (
      nextValue: StateType[P],
      lastValue: StateType[P]
   ) => void;
};

export function createPropertyCallback<StateType>(
   provider: ReactNProvider,
   propertiesObj: IPropertyConfig<StateType>
) {
   const lastValues: Partial<StateType> = {};
   const properties = Object.keys(propertiesObj).map(property => ({
      property,
      callback: propertiesObj[property]
   }));

   properties.forEach(({ property }) => {
      lastValues[property] = provider.getGlobal()[property];
   });

   return globalState => {
      for (let prop in lastValues) {
         const nextValue = globalState[prop];
         const previousValue = lastValues[prop];

         if (!isEqual(nextValue, previousValue)) {
            lastValues[prop] = nextValue;
            properties
               .find(x => x.property === prop)
               .callback(nextValue, previousValue);
         }
      }
   };
}

A couple differences in mine...

  • It is set up to allow one or many property callbacks.
  • The provider is a required argument so that the function can set an initial value during setup.
  • The callback sends both the nextValue and previousValue as arguments.
  • I am using isEqual from underscore for comparison.

I only use ReactN with createProvider so for someone using the regular global, I imagine they can just pass in the global state as the "provider", but I haven't tested that.

Here's how you use it:

interface ITestState {
   count: number;
   status: string;
   dontCare: string;
}
const TestState = createProvider<ITestState>({
   count: 0,
   status: 'hello, world',
   dontCare: 'not interested in this one'
});

TestState.addCallback(
   createPropertyCallback<ITestState>(TestState, {
      count: (n, p) => {
         console.log(
            'count changed',
            `previous value: ${p}`,
            `next value: ${n}`
         );
      },
      status: (n, p) => {
         console.log(
            'status changed',
            `previous value: ${p}`,
            `next value: ${n}`
         );
      }
   })
);

@quisido
Copy link
Collaborator

quisido commented Jan 29, 2020

Thank you very much for the contribution. I hope others find it and resolve their similar issues. I'll leave this open as a note for the official implementation, but I don't see it happening anytime soon, as I mentioned earlier. :)

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
feature A new feature is requested.
Projects
None yet
Development

No branches or pull requests

2 participants