smapiot / piral

🚀 Framework for next generation web apps using micro frontends. ⭐️ Star to support our work!
https://piral.io
MIT License
1.68k stars 125 forks source link

Document Data Sharing from a Piral Instance #104

Closed kpyfan closed 4 years ago

kpyfan commented 4 years ago

New Feature Proposal

For more information, see the CONTRIBUTING guide.

Description

Currently we allow pilets to share data using the Piral API by calling setData and getData to store into some global key/value store. I'd like to see the ability to have some data set into this by the base piral instance on render.

Background

The use case for this is for sharing user information to pilets. Currently, we expect the instance to render only to authenticated users. We pull some user information (such as email) from an API when rendering the Menu component in order to generate portions of the menu. When we do so, we'd like to have the ability to make the user information available to all the other pilets.

Discussion

Biggest pro here is being able to boootstrap data from the main piral instance and have that piral instance manage the data for the other pilets. This is useful for any sort of global data that isn't necessarily spawned/acquired by a specific pilet.

FlorianRappl commented 4 years ago

I think what you want can be done with the Piral instance.

Either you use the "root" pilet, e.g.,

const instance = createInstance({ ... });
instance.root.setData('foo', 'bar');

or you just use the action, e.g.,

const instance = createInstance({ ... });
instance.context.writeDataItem('foo', 'bar');

The latter also allows you to set expiration and owner directly and will always write. The former behaves exactly as if a pilet would write.

(The examples only show setData, but getData works the same)

Alternatively, you can also just initialize the state properly.

const instance = createInstance({
  state: {
    data: {
      foo: {
        value: 'bar',
        owner: '',
        target: 'memory',
        expires: -1,
      },
    },
  },
});

However, the best way to share functions (or information) is to provide an API from the Piral instance.

function createCustomApi() {
  // return a constructor using the global context
  return context => {
    // return a constructor for each local API using the pilet's metadata
    return (api, meta) => ({
      foo: 'bar', // this is the API to return; just a "static" foo - but you could have functions, etc.
    });
  };
}

const instance = createInstance({
   extendApi: [createCustomApi()],
});

If its really about the render part and you'd not like to change the data initially, you can also go ahead as such


const MyComponentInPiral = () => {
  const setData = useAction('writeDataItem');
  setData ('foo', 'bar');
  return <div>Render something</div>;
};

Is that what you want?
FlorianRappl commented 4 years ago

Documentation update landed in develop.

Still - if you have some other need here that was not covered in the documentation (or in Piral at all) let us know!

kpyfan commented 4 years ago

Yeah, this looks like roughly what I want. Right now I'm loading in the data the componentDidMount function on my menu component, but that's easy enough to move.

At the moment, my index.tsx looks like this:

import * as React from 'react';
import {renderInstance, setupGqlClient} from 'piral';
import {layout, errors} from './layout';

const axios = require('axios');

renderInstance({
  settings: {
    gql: setupGqlClient({
      subscriptionUrl: false,
    }),
  },
  requestPilets() {
    let promise = axios.get('/service/v1/pilets');
    let items = promise.then(res => res.data)
    .then(res => res.items);
    return items;
  },
  layout: layout,
  errors: errors,
});

export * from 'piral';

Since renderInstance calls createInstance and returns the instance itself, I can just use the return value of renderInstance instead here and set my data. What would be the best way to get this info into my component then? Do we have the ability to get the current piral instance api as a part of one of my components? Or can I pass it down through my layout somehow?

FlorianRappl commented 4 years ago

Yeah so this file looks pre-Piral v0.9.

So first I'd like to show the migration path (then I'll discuss what you can without migration).

After migration your file would look:

import * as React from 'react';
import {renderInstance} from 'piral';
import {layout, errors} from './layout';

const axios = require('axios');

renderInstance({
  requestPilets() {
    let promise = axios.get('/service/v1/pilets');
    let items = promise.then(res => res.data).then(res => res.items);
    return items;
  },
  layout,
  errors,
});

(I don't think you use GraphQL - so happy news for you: piral-urql is not opt-in and thus there is no need to disable subscriptions; they are not available when you did not add and set up piral-urql explicitly)

Also notice that the export is gone.

Alright, so far so good - but how does that help us regarding an instance? Well, renderInstance gets a configuration to call createInstance under the hood. Actually, it calls createPiral, see:

const instance = createPiral(config, settings);

createPiral is just a wrapper to call createInstance with the API extensions from piral-ext. So how does the whole story help you? Guess what... renderInstance also returns the created instance.

So instead of

const instance = createInstance({ ... });
instance.root.setData('foo', 'bar');

you can also do

const instance = renderInstance({ ... });
instance.root.setData('foo', 'bar');

It's the same. I think you also had it that far:

Since renderInstance calls createInstance and returns the instance itself, I can just use the return value of renderInstance instead here and set my data. What would be the best way to get this info into my component then? Do we have the ability to get the current piral instance api as a part of one of my components? Or can I pass it down through my layout somehow?

There are two options, but I would just go with the one I outlined above (so no need to pass down anything):

const MyComponentInPiral = () => {
  const setData = useAction('writeDataItem');
  setData ('foo', 'bar');
  return <div>Render something</div>;
};

Would that work?

Hope that helps!

kpyfan commented 4 years ago

Thanks for the help! I'll give it a shot and see if I run into any other issues.

kpyfan commented 4 years ago

Alright, so I've run into one minor issue here - on a deployed instance where latency is higher, we've got a race condition with the way I'm doing things. I wanted to pick your brain on the correct way to handle this situation.

In the index.js for the Piral instance, we're making an XHR call using Axios. In the promise, we're calling instance.root.setData('foo', 'bar'); to store the result of the XHR call as data that the pilets can access.

As a part of the base layout (in the menu), we're passing in the instance above to the menu as a prop. What we want to do here is set the state in the menu using the data from the XHR call above. However, sometimes the component renders before the XHR call has returned, resulting in no data being available. As best I can tell, the better way to do this would be to have the setting of data being done in a componentDidUpdate function on the menu. However, I've no luck in trying to get the component to detect the data changing for the key as an "update". Any suggestions on how to properly update the state of the menu each time this data is updated?

FlorianRappl commented 4 years ago

If your component lives anyway in the app shell (sounds like it for the menu) then instead of passing in a prop "couple" it to the global state container.

const Menu = () => {
  const data = useGlobalState(m => m.data.foo);
  return <b>Current value: {data && data.value}</b>;
};

Instead of the logic operation you could also use data?.value if you already use the next gen / latest version of TypeScript.

Since this way you are "connected" to the global state container your component will re-render on change.

Hope that helps!