Giter VIP home page Giter VIP logo

resift's People

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

Forkers

r17x

resift's Issues

Proposal: Guard

One of the hardest problems about data fetching in components is dealing with null values gracefully.

I originally thought suspense would be our silver bullet to everything against falsy data checks but that's not exactly the case. The issue with Suspense is that if a certain data requirement is not met, the whole component will be blocked from rendering. This not ideal in situations where you rely on a certain part of the component to be mounted for animations or layout structure and size.

For example, let's say you have a material-ui Drawer component. This component should always be mounted regardless if there is data ready because of the animation in and the animation out. We shouldn't block the whole component because then we wouldn't be able to see the drawer animate in.

You could argue that we should just split up the components so that there is a suspending part and a non-suspending part but, imo, that division feels slightly clunky.

e.g.

import React from 'react';
import { useDispatch } from 'resift';

function SuspendingComponent({ exampleFetch }) {
  const dispatch = useDispatch();
  const data = useSuspendingFetch(exampleFetch, () => dispatch(exampleFetch()));

  return <div>{data.stuff}</div>
}

function NonSuspendingComponent({ id }) {
  const exampleFetch = makeExampleFetch(id);
  
  return <Drawer>
    <Suspense fallback={<CircularProgress />}>
      <SuspendingComponent />
    </Suspense>
  </Drawer>;
}

It feels especially clunky having to pass some sort of prop down to the suspending component.


Anyway, I don't think that'll work so let's try something else.

This issue proposes: useGuard

useGuard is a custom hook that takes in a fetch and returns a component that takes in a render prop. That render prop will only be invoked when there is data available and will provide the data as its argument.

e.g.

function UseGuardExample({ id }) {
  const exampleFetch = makeExampleFetch(id);
  const status = useFetch(exampleFetch)[1];
  const Guard = useGuard(exampleFetch);

  return <Drawer>
    {isLoading(status) && <CircularProgress />}
    <Guard>{data => <div>{data.stuff}</div>}</Guard>
  </Drawer>;
}

Implementation:

import { useMemo } from "react";
import { useFetch, isNormal } from "resift";

function useGuard(fetch) {
  return useMemo(() => {
    function Guard({ children }) {
      const [data, status] = useFetch(fetch);
      return isNormal(status) && children(data);
    }

    return Guard;
  }, [fetch]);
}

export default useGuard;

Allow more specification to the loading status behavior

@pearlzhuzeng had experienced an unexpected result regarding the LOADING status when developing the ReSift Rentals demo.

The demo is application is structured like Netflix's list of lists of movies. She had a few fetches that we were shared and they were also using the newer "merges across namespaces" feature.

The issue and question is: what is the status of a fetch that has a share across a namespace?

Here's the scenario, you have three fetches with two different namespaces:

  • makeGetGenre - a fetch factory that gets a list of movies. namespace: genre
  • makeGetMovie - a fetch factory that gets a single movie. namespace: movie
  • makeUpdateMovie - a fetch factory that updates a single movie. namespace: movie

makeGetGenre implements a merge for the namespace movie so that when a movie item changes, the genre list changes to reflect that change.

The question is, what is the status of a genre when a movie is inflight?

// after you dispatch an update...
const updateMovie = makeUpdateMovie(movieId);
dispatch(updateMovie());
// ...what is the status of the genre list?
const getGenre = makeGetGenre(genreId);
const status = useStatus(getGenre);

// what should this be??
console.log(isLoading(status));

Through the current implementation, the output of isLoading is true.

With this behavior, whenever any movie goes inflight, it will cause all lists to have the status LOADING. This is because the genre namespace doesn't know which movie belongs to which genre (and you can't know ahead of time).

This can be confusing to the developer because an update to a movie in a different genre would cause all the genres to update (which is what @pearlzhuzeng experienced). I think this behavior should not be the default and we should add other behavior options.


The proposal is to remove the option isolatedStatus and replace it with type.

The possible types of values of type will be:

  • self - only consider the status of the current fetch factory + key
  • namespace - only consider the statuses of all fetches related the current namespace + key
  • all - consider the statuses of all the fetches related to the current namespace + key as well as any other namespace (e.g. genre namespace and movie).
const selfStatus = useStatus(getGenre, { type: 'self' }); // this is the new `isolatedStatus: true`
const namespaceStatus = useStatus(getGenre, { type: 'namespace' });
const allStatus = useStatus(getGenre, { type: 'all' });

I think the default type for useStatus should be namespace.


That's it! Let me know if you need any further clarification. I think @pearlzhuzeng knows what I'm talking about though.

Brainstorming APIs for Suspense

TIL that suspense for data fetching is sort of working in React already by simply throwing a promise.

It's undocumented but looking at react-cache (the experimental library demoed at React Conf 2018) shows that it works by throwing a promise.

And I've also created this demo as a quick POC of the mechanism.

I think this is a good indication of how suspense will work in the future and we can probably start brain storming resift APIs given this mechanism.


Current state:

We currently do not suspend rendering if data isn't present. Instead useFetch returns null for the data and UNKNOWN for the status and it's up to the component to do the guard against null data as well as the initial dispatching to kick off the flow.

import React, { useEffect } from 'react';
import { useFetch, isNormal, useDispatch } from 'resift';
import makeMyFetch from './makeMyFetch';

function MyComponent({ id }) {
  const dispatch = useDispatch();
  const myFetch = makeMyFetch(id);
  const [data, status] = useFetch();

  useEffect(() => {
    dispatch(myFetch());
  }, [myFetch]);

  // prevent rendering if no data
  if (!isNormal(status) {
    return <Spinner />
  }

  // when there is data then render...
  return <div>{data.stuff}</div>
}

Possible next state:

We can create a new custom hook that will suspend rendering if the status does not contain NORMAL. This new hook will need to throw a promise that when resolved, will be sufficient to tell Suspense the component is ready to be rendered again without the throw.

I'm imagining something like this:

function useSuspendingFetch(fetch, dataReadyNotifier) {
  const result = useFetch(fetch);
  const [data, status] = result;

  if (!isNormal(status)) {
    throw dataReadyNotifier();
  }

  return result;
}
function MyComponent({ id }) {
  const myFetch = makeMyFetch(id);
  const dispatch = useDispatch();

  // data will never be `null` because this component will be Suspended in that case
  const [data, status] = useSuspendingFetch(myFetch, () => dispatch(myFetch()));

  return <div>{data.stuff}</div>;
}

Interestingly enough, this simple thing works right now!

Code sandbox demo

This works because:

  • ReSift's dispatch returns a promise when a request is dispatched
  • useSuspendingFetch throws that promise that React.Suspense will catch and await
  • when the promise is resolved, the status will contain NORMAL and the data will be present

So that's that. Let me know what y'all think!

I think it would be cool to ship an unstable_useSuspendingFetch in one of our upcoming releases.

Checking and manipulating status

I have an issue with the naming of our helper functions isNormal, isLoading etc.

The prefix is is where I have the issue. The function isNormal does not check if the status is exactly normal, instead checks to see if that status contains normal.

So let's rename it? πŸ€”

isNormal => hasNormal

function Component() {
  const [data, status] = useFetch(myFetch):
  return <>
    {hasLoading(status) && <Spinner />}
    {hasNormal(status) && <>{data.stuff}</>}
  </>;
}

I think this helps communicate to our users that statuses are a collection of booleans vs a mutually exclusive state.


Manipulating a status (to force a certain status for the loader for example), currently requires some knowledge about bitwise operators which not great. Instead it would be better if we has manipulation methods like addNormal, removeNormal or similar.

Alternatively we could add a function addStatus(status, NORMAL) to reduce the API footprint.

Hoist merges to top-level

Problem

One of the issues with the current API of ReSift is that it's ambiguous which merge function will run when two fetch factories implement a merge from one namespace

For example:

getMovies.js

import { defineFetch } from 'resift';

const makeGetMovies = defineFetch({
  displayName: 'Get Movies',
  share: { namespace: 'movieList' },
  make: /* ... */,
});

const getMovies = makeGetMovies();
export default getMovies;

makeGetMovie.js

import { defineFetch } from 'resift';

const makeGetMovie = defineFetch({
  displayName: 'Get Movie',
  share: {
    namespace: 'movieItem',
    merge: {
      // this is merge 1 πŸ‘‡
      movieList: (prevMovie, nextMovieList) => /* ... */,
      // πŸ‘†πŸ‘†πŸ‘†
    },
  },
  make: /* ... */,
});

makeUpdateMovie.js

import { defineFetch } from 'resift';

const makeUpdateMovie = defineFetch({
  displayName: 'Update Movie',
  share: {
    namespace: 'movieItem',
    merge: {
      // this is merge 2 πŸ‘‡
      movieList: (prevMovie, nextMovieList) => /* ... */,
     // πŸ‘†πŸ‘†πŸ‘†
    },
  },
});

So in this situation, there are 2 namespaces: movieList and movieItem and there are 3 fetch factories: getMovies, makeGetMovie, and makeUpdateMovie.

The ambiguity comes from the following question: when a SUCCESS occurs the movieList namespace (i.e. the getMovies fetch factory), which merge ("merge 1" or "merge 2") is ran?


The answer to this question is actually which ever merge was associated with a fetch factory that was dispatch last β€” a convoluted and weird answer.

Proposal

Instead of having the merges in the fetch factories, I propose we hoist them up to the <ResiftProvider> using a merges property. The convention would be to create a new file or folder called merges that will handle doing the merging from all namespaces to other namespaces.

index.js

import React from 'react';
import { render } from 'react-dom';
import { ResiftProvider } from 'resift';
import App from './App';

import dataService from './dataService';
import merges from './merges';

const container = document.querySelector('#root');

render(
  <ResiftProvider dataService={dataService} merges={merges}>
    <App />
  </ResiftProvider>,
  container
);

merges.js

const merges = {
  incomingNamespace: {
    destinationNamespace: /* ... mergeFunction ... */,
  },

  movieList: {
    movieItem: (previousMovieItem, nextMovieList) => {
      // return a new movieItem using the `nextMovieList`
    }
  },
};

export default merges;

The proposal is simple: instead of having the merges on the fetch factory, hoist them to the provider.

This will actually make the code much more simple. We can remove a lot of confusing logic in the reducers.

const mergeState = { ...state?.merges };
// (eslint bug)
// eslint-disable-next-line no-unused-vars
for (const [targetNamespace, mergeFn] of Object.entries(mergeObj)) {
const merges = { ...mergeState?.[targetNamespace] };
merges[namespace] = mergeFn;
mergeState[targetNamespace] = merges;
}

πŸ‘†this code snippet is creating a lookup table of merge objects on FETCH so that it can correctly know how to merge in an incoming SUCCESS. If that sounds convoluted, it's because it is πŸ˜….

If we hoist the merges to the provider level then we can deprecate this code and make things a bit more straightforward.

Summary

Problem: It's ambiguous which merge function will run when there is more than one merge function for the same source-target pairing of namespaces.
Solution/Proposal: Hoist the merges definition to the provider.
API Changes: share.merge will be deprecated (or possibly removed if possible) in favor of adding a merges prop to the <ResiftProvider> component.

Bundle Size and Bundling

This issue is to track to two things:

  • bundle size
  • how we bundle

bundle size

The bundle size in ReSift is not great, I think it's acceptable but we could do better.

According to bundlephobia, ReSift is 13.9kB minified and zipped.

However, we currently require two peer deps, redux 2.6kB and react-redux 5.5kB brining the total footprint of ReSift to around ~22kB (given that you're not already using react + react-redux).

Here is the result of running webpack-bundle-analyzer (without redux or react-redux):

image

My biggest takeaways:

  • superagent (http client) eats up too much of the budget. Bundlephobia says it's 6.2kB. If we remove that and replace it with a simpler XHR equivalent, we'll reduce the bundle by nearly 50%. Note that we can't replace Superagent with window.fetch because ReSift needs the ability to cancel the request.
  • shortid 1.1kB can probably be swapped out with nanoid 0.3kB very easily
  • useStatus on our side is relatively large and can be reduced in size easily

If we do those, we can shave off 7-8kB off the bundle taking the ReSift core bundle (without Redux) to be around 6.4kB and with Redux, around 14.5kB.

I think these items are a good first iteration goal of reducing bundle size. There will be more investigation of removing redux and react-redux for the purposes of concurrent mode in #32 but for now, I don't think removing redux is worth focusing on for a stable release.

how we bundle

we currently bundle using webpack with the library option of UMD. My current understand is that this way of bundling does not support tree-shaking (though in reality, you'll probably end up using all of ReSift so tree shaking is somewhat irrelevant).

I know other libraries like react-router use rollup. I'd like to switch as well. In general, we just need a bundling audit.

Context Loader

We've had a reoccurring issue with double spinners where there are two loading spinners are on the page for similar requests.

I think we can solve this issue using subscription mechanisms + context.

We can create a component (for lack of a better name) <ContextLoader> that adds a context provider over its children. If we use another <ContextLoader> inside of this loader, if the parent loader is loading, then it will prevent any of the children loaders from revealing.


I don't have an implementation ready at this point but my current thought is that the context shape will be some sort of set. Each time a nested provider is added to the tree, it will add something that allows the child component to pull its state.

When a loader tries to render a view, it will first check the state of the parents.

Roadmap/Backlog

Hey all, I have a tentative for roadmap for ReSift!

This list isn't set in stone and there may be other things added later but this should communicate the prioritizes and the order of things to be released.

I think it makes a lot of sense to adopt an experimental channel like react has adopted to align with the features they've released in that channel.

Supporting Concurrent Mode and Suspense #32 is definitely a high priority, however, realistically, we can't have a stable release until React has a stable release. There will be a lot of work in the stable/latest channel alongside the experimental channel.

Here is a very tentative roadmap:

Stable

0.1.1

  • add a type or similar option to useStatus e.g. useStatus(getPerson, { type: 'self' }).
    The allowed types will be something like self, namespace, and all. This will deprecate isolatedStatus (issue coming soon)

0.2.0

  • (tentative) rework merge across namespace APIs (issue coming soon)

0.2.1

  • debouncing
  • user cancelation (e.g. useCancel)

0.3.0

  • (very tentative) reduce bundle size via removing superagent and/or redux #36

Experimental

  • Support Suspense and SuspenseList (this may or may not require removing redux)
  • (very tentative) Use useTransition and useDeferred value internally in dispatch and useData (respectively) (this will probably require removing redux)

Initial release πŸŽ‰ (v0.1.0) roadmap/todo list

Stability is something we care about deeply with this library so it's important for us to have a plan/todo list that includes everything that's required in order to finish the initial release of this library.

The good news is that the code for this library is done (since we use it in production at Sift), however there is still a list of things to complete for our initial release.

Here's that list:

  • Documentation of main concepts and main APIs (most effort by far) See #7
  • New README
  • Automatic API documentation generation + JS doc audit + integration with docusaurus
  • Test coverage > 95% (ideally 100%). See #11
  • Coveralls + Travis integration
  • API audit for older terminology before fetch factories and fetches instances (any terminology that uses "action creators" should be removed)
  • Add eslint back in + linting audit using react-hooks/exhaustive-deps
  • Typings audit
  • Semi-permanent logo + colors
  • (nice-to-have) Tutorial using ReSift Rentals
  • #21
  • #23
  • #22
  • SEO audit

Please let me know if there is anything else missing for this list or if you have any insight in any of the tasks above.

Thanks everyone!

Set a default for the `key` property on the make function in `defineFetch`

export default defineFetch({
  displayName: 'Get People from listId',
  make: (listId: string) => ({
    key: [listId],
    request: () => ({ http }) =>
      return http({
        method: 'GET',
        route: `/list/${listId}/members/`,
      }),
  }),
});

From all the times I've used Resift, I have never had to deviate from what I passed into the make value, to then into the key value.

To keep things more simple for Resift, I purpose default the key value to be the arguments passed into the make function. If there needs to be an override, be explicit and declare the key value to be something unique.

Resift will not render on Android React Native projects with hermes enabled.

Hey Rico, letting you know about this issue and in no way do I feel you are responsible for fixing this. In the short term, Sift will disable hermes because we do not debug from android devices very often.

The Issue

When resift library is import'ed on an android react native application with hermes enabled, the application will error out saying "React Native Hermes: undefined is not a function js engine: hermes"

Seems like Hermes does not want to render the build file from resift. And due to the entire resift library being on one file, it is rather challenging to quickly see where in the build index.js file is there not a function defined.

What is Hermes

Hermes is an open-source JavaScript engine optimized for running React Native apps on Android. For many apps, enabling Hermes will result in improved start-up time, decreased memory usage, and smaller app size. At this time Hermes is an opt-in React Native feature, and this guide explains how to enable it.

https://reactnative.dev/docs/hermes

Tutorial: ReSift Rentals

This issue exists to track progress on the ReSift tutorial created by @pearlzhuzeng .

Her docs were merged into the v0.1.0 branch however they are not currently in the sidebar. We'll use this issue to track progress.

  • task one
  • task two

API documentation

This issue is here to track how we'll do API documentation.

I think we should try experimenting with semi-automatic API documentation using typedoc since we already have typings in our repo but I'm also open to ideas on how to handle this.

Typedoc doesn't work for .d.ts files in my experiments so I opted to write some parsers. Check it out in #13 .

This issue is here to experiment and try different ways of doing API documentation.

Jules also started some manual API docs that we can migrate over to the autogenerated versions see #1

Thoughts are appreciated!

Adding ESLint + linting

The linter and the linting rules were not carried over from resift-internal in order to save time, however that should definitely be done.

This issue exists to determine:

  1. what lint rules should we use, (one required rules is probably react-hooks/exhaustive-deps though)
  2. using those rules, what do we have to change (e.g. do the actual linting)

This issue exist to track the progress on this.

Initial documentation

This issue is here to track the ReSift docs. Here is a tentative outline for the docs.

Feel free to suggest any additions or take ownership of any particular doc.

Introduction (required docs):

  • What is ReSift?
  • Quick glance
  • Installation

Main concepts (required docs):

  • What's a fetch? β€” introduces the concept of a fetch and explains the basic lifecycle
  • How to define a fetch? β€” gives an in-depth tutorial for how to define a fetch factory
  • Sharing fetches β€” introduces the share API and some use cases for it
  • Making sense of statuses β€” introduces the status in depth. Should probably provide a recipe for our Loader component
  • What are data services? β€” introduces the concept of data services and gives a tutorial on how to define custom data services. This doc should also dive deeper into how the cancellation mechanism works
  • Error handling β€” introduces the useError hook, the ERROR status, try catching dispatching, and the onError callback in the data service parameters

Examples:

These would be great as code-sandboxes.

  • CRUD example (@JulesPatry also has some of this started in #1)
  • Infinite scrolling

Tutorial (nice-to-have):

API docs (required but should be auto generated):

  • auto generated from typings. see #8

Error handling improvements: add useError

The lists feature showed us that we have a lot of work to do in order to get error handling right in ReSift. The current philosophy is that we should treat errors as rare events that cause the data to "stop flowing" and potentially crash the page.

Lists proved to us that that is definitely not the case. Errors should be accepted as a common use case and we should structure ReSift to handle errors better.


In the current state of ReSift, the way to handle errors is to try catch them in either the fetch factory request body OR where we called dispatch

e.g.

in the fetch factory request body:

const makePersonFetch = defineFetch({
  displayName: 'Get Person',
  make: id => ({
    key: [id],
    request: () => async ({ http }) => {
      try {
        await http({/* ... */});
      } catch (e) {
         // do something with e
      }
    },
});

where we called dispatch

const handleThing = () => {
  try {
    await dispatch(/* ... */);
  } catch (e) {
    // do something with e
  }
};

Also, once an error happens, useFetch will return null instead of the data. Also there is no way to get the error body/data from the response once an error occurs.


Anyway, we have a lot of problems to solve so here is yet another proposal:

useError

useError is a custom hook that will return any thrown error in a fetch factory's request function.

const MyComponent({ id }) {
  // ...
  const personFetch = makePersonFetch({ id });
  const error = useError(personFetch);

  return <div>
    {error && <ErrorBanner>{error.message}</ErrorBanner>}
    {/* ... */}
  </div>;
}

useError also aligns with #21

useFetch does too much; add `useData` and `useStatus`

When originally designing the API for useFetch, I thought there would be seldom cases for when you would only need the status of a fetch without needing the data.

However, with the addition of proposal #20 useGuard, it would become a much more common case where you would only need status.

So why not split up useFetch into two separate hooks? It seems to be what the community is doing (looking at the new hooks API from react-redux and react-router are doing).


How does adding useData and useStatus sound?

How to handle local state changes

Hey,

I'm intrigued by this library, but I'm not yet clear on whether it would fit our use case. Currently, coordinating all the different fetches and their state is a big headache, which ReSift seems to have a good solution for.

I'm curious however how I'd incorporate stuff like optimistic updates (immediately setting state, then potentially rolling back if api says nay) and local state changes which do not involve communicating with a server in general.

Sharing fetches across different namespaces

ReSift shares are essential features in order to keep consistency across our apps. However, I've noticed a problem with the current ReSift shares model and I'd like to propose a non-breaking solution for this.

⚠️This is a proposal ⚠️

Problem

Background

Let's say you have a fetch that represents a collection of things.

moviesFetch.js

const { defineFetch } from 'resift';

const makeMoviesFetch = defineFetch({
  displayName: 'Get Movies',
  make: () => ({
    key: [],
    request: () => ({ http }) => http({
      method: 'GET',
      route: '/movies',
    }),
  }),
});
const moviesFetch = makeMoviesFetch();
export default moviesFetch;

And you also have another fetch that represent getting one of those things from that collection

makeMovieFetch.js

import { defineFetch } from 'resift';

const makeMovieFetch = defineFetch({
  displayName: 'Get Movie',
  make: id => ({
    key: [],
    request: () => ({ http }) => http({
      method: 'GET',
      route: `/movies/${id}`,
    }),
  }),
});

export default makeMovieFetch;

And then you have one more fetch that updates a movie:

makeUpdateMovieFetch.js

import { defineFetch } from 'resift';

const makeUpdateMovieFetch = defineFetch({
  displayName: 'Update Movie',
  make: id => ({
    key: [id],
    request: updatedMovie => ({ http }) => http({
      method: 'PUT',
      route: `/movies/${id}`,
      data: updatedMovie,
    }),
  }),
});

The issue

The issue happens when you try to share all of these fetches. Each fetch is related because they come from the same collection, however, sharing them may lead to undesired results when pulling data.

When fetches are shared, they share the same store and this means that the moviesFetch (i.e. the list) will share the same state as the getting a single movie makeMovieFetch (i.e. an item from the list).

If you already have these fetches in use and you want to share the fetches to ensure consistency, then sharing the fetch would require changing the component's use of useFetch because the shape of the data coming back is different between moviesFetch and makeMovieFetch.

e.g.

before the share

function Movie({ id }) {
  const movieFetch = makeMovieFetch(id);
  const [data] = useFetch(movieFetch);

  console.log(data); // { title: 'Paddington 2 } (just one movie)
}

after the share with movies

function Movie({ id }) {
  const movieFetch = makeMovieFetch(id);
  const [data] = useFetch(movieFetch);

  console.log(data); // [{title: ...}, {title: ...}]; now it's a collection of movies??
}

Root cause

I think the root cause of this problem is forcing shares to have the same shape.

Solution

The solution to this problem is allow fetches that have different shapes to have separate state but allow fetches to watch for changes from other namespaces.

The API I'm proposing will look like this:

moviesFetch.js

import { defineFetch } from 'resift';

const makeMoviesFetch = defineFetch({
  displayName: 'Get Movies',
  share: {
    namespace: 'movieList',
    merge: {
      // movieList: (previousMovies, nextMovies) => nextMovies, // this is the default behavior 
      movieItem: (previousMovies, updatedMovie) => {
        const index = previousMovies.find(movie => movie.id === updatedMovie.id);
        const nextMovies = [
          ...previousMovies.slice(0, index),
         updatedMovie,
         ...previousMovies.slice(index + 1, previousMovies.length),
        ];
      },
    },
  },
  make: () => ({
    key: [],
    request: () => ({ http }) => http({
      method: 'GET',
      route: '/movies',
    }),
  })
});

makeUpdateMovieFetch.js

import { defineFetch } from 'resift';

const makeUpdateMovieFetch = defineFetch({
  displayName: 'Update Movie';
  share: {
    namespace: 'movieItem',
    merge: {
      movieList: (previousMovie, movieList) =>
        movieList.find(movie => movie.id === previousMovie.id),
    },
  },
});

The idea here is that share.merge can now be an object and react to changes in other namespace by using their namespace key in merge.

If the default behavior of the share is to be overridden then its own namespace key should be used as the merge key.

Let me know what y'all think! I think this could be very helpful for the Sift Lists feature on both web and mobile.

Code examples page

@pearlzhuzeng had a great idea for creating a page that shows a bunch of code examples for ReSift. She suggested we create a page similar to react-spring's example page so we can show off our cool demos and allow the community to add their own.

Fetch naming conventions

A while ago @JulesPatry had some ideas regarding changing the naming convention of fetches which have a lot of appeal.

He suggested that we drop the -fetch post-fix in favor of always having get, update, create, and delete in the name of the fetch.


current convention:

const makePersonFetch = defineFetch({/* ... */});
const personFetch = makePersonFetch(id);
const [person] = useFetch(personFetch);
const makeUpdatePersonFetch = defineFetch({/* ... */});
const updatePersonFetch = makeUpdatePersonFetch(id);
const [person] = useFetch(updatePersonFetch);

proposed convention (credit to jules):

const makeGetPerson = defineFetch({/* ... */});
const getPerson = makeGetPerson(id);
const [person] = useFetch(getPerson);

useEffect(() => {
  dispatch(getPerson());
}, [dispatch, getPerson]);
const makeUpdatePerson = defineFetch({/* ... */});
const updatePerson = makeUpdatePerson(id);
const [person] = useFetch(updatePerson);

useEffect(() => {
  dispatch(updatePerson());
}, [dispatch, updatePerson]);

Jules's convention works because of the addition of the CRUD verb. We know that a fetch is a fetch if it has a CRUD verb so we can get rid of the fetch post-fix.

Suspense and Concurrent Mode in ReSift

The React team has finally released docs for suspense for data fetching and concurrent mode πŸŽ‰. They've clarified a bit of how it works and how its intended to be used, and my first impression is that it can actually fit within our current APIs nicely without any big breaking changes.

Reading their docs is a prereq to this issue. and so is watching this talk from the Relay team.

They say the correct way to use suspense is to "render as you fetch", meaning that you reveal parts of the API that make sense to reveal if you have enough data to reveal it. Here is a direct quote from their new docs:

Although it’s technically doable, Suspense is not currently intended as a way to start fetching data when a component renders. Rather, it lets components express that they’re β€œwaiting” for data that is already being fetched.

which sums up the idea of "render as you fetch".


What does this mean for ReSift?

Well, fortunately, not that much. Many simple fetching libraries rely on "fetch-on-render" techniques because the state of the fetches inflight live in the component. Fortunately for ReSift, the state of our fetches lives in global state, and we've already discovered that we need to split up fetching (e.g. dispatch(getPerson())) from getting the data (e.g. useData(getPerson)).

This means that we can advise users to initiate fetches for components at some higher component level to abide by the "already being fetched"/"fetch-as-you-render" philosophy. This is possible because our useDispatch and useData APIs are separate.

Initial thoughts on Suspense and Concurrent Mode in ReSift (revised)

Here's how I think it should work (disclaimer: i have not tested to see if this will work):

  1. useData should suspend by default (i.e. throw a promise) and we should add a configuration object with the flag noSuspend (or maybe allowNull or similar) to allow the hook to return null instead of causing suspense (e.g. useData(getPerson, { noSuspend: true })).
  2. we advise our users to dispatch higher up in the tree to prevent "fetch on render" patterns*
  3. we make dispatch work with startTransition of useTransition

I still have to test whether this approach makes sense but there's my initial thoughts on how that should work.

Adoption strategy/road map concerns

Suspense + concurrent mode should stay in an experimental as long as React has them in an experimental channel. See #37 for more details.

Loader boundaries

TL;DR this is a proposal enables us to write code like this:

import React from 'react';
import thingFetch from './thingFetch';
import { useData, withLoaderBoundary } from 'resift';

function Thing() {
  // thing will always be fetched
  const [thing] = useData(thingFetch, dispatch => {
    dispatch(thingFetch());
  });

  return <div>{thing.stuff}</div>
}

export default withLoaderBoundary(Thing);

ReSift will now handle dispatching fetches for you and (unlike useFetch) the data useData returns will always be defined and fetched like magic.


Not TL;DR

So I was reading Dan Abramov's article on "algebraic effects" and in that article he linked a tweet/gist from Sebastian MarkbΓ₯ge.

This gist implements a way to get values from promises without using .then() and without using async/await which is pretty fascinating/game changing.

Check it out:

usage:

function getUserName(id) {
  var user = JSON.parse(fetchTextSync('/users/' + id));
  return user.name;
}

function getGreeting(name) {
  if (name === 'Seb') {
    return 'Hey';
  }
  return fetchTextSync('/greeting');
}

function getMessage() {
  let name = getUserName(123);
  return getGreeting(name) + ', ' + name + '!'; 
}

runPureTask(getMessage).then(message => console.log(message))

implementation:

let cache = new Map();
let pending = new Map();

function fetchTextSync(url) {
  if (cache.has(url)) {
    return cache.get(url);
  }
  if (pending.has(url)) {
    throw pending.get(url);
  }
  let promise = fetch(url).then(
    response => response.text()
  ).then(
    text => {
      pending.delete(url);
      cache.set(url, text);
    }
  );
  pending.set(url, promise);
  throw promise;
}

async function runPureTask(task) {
  for (;;) {
    try {
      return task();
    } catch (x) {
      if (x instanceof Promise) {
        await x;
      } else {
        throw x;
      }
    }
  }
}

And then someone replied to this tweet implementing the same concept but using React with error boundaries:

image


What's going on here?

The code example above is getting values from promises "synchronously" via hacking the throw statement. When a component encounters these pseudo promises, it throws stopping the current execution. However, when the promise actually resolves, there's a loop that will re-try the execution. Since there is data, the flow will execute without throwing.

Since the React components are pure/idempotent, we can have them partially re-execute without the worry of side-effects.

When you add React's error boundaries with componentDidCatch, we can implement this idea of "loader boundaries".


Loader Boundaries: A game changing proposal

Loader boundaries are components that will prevent rendering of components with fetches until the values from those fetches become defined. What these boundaries will do is "catch" thrown fetches and prevent rendering until those fetches are finished.

This enables us to write hooks that will always assert that data is there and furthermore we can make these hooks calls dispatch fetches for data if there is none.

The goal of this proposal is to have this hook useData (tentative name) that will:

  • dispatch the a request for the fetch if the data isn't there yet
  • return the result of your fetch like it's already there and fallback to the closest loader boundary
function Person({ personId }) {
  const personFetch = makePersonFetch(personId);

  // person will always be defined
  // this hook will also dispatch a request if there isn't an inflight one yet
  const [person] = useData(personFetch, dispatch => {
    dispatch(personFetch())
  });

  return <div>{person.name}</div>;
}

function App() {
  <LoaderBoundary fallback={<CircularProgress />}>
    <Person id="person123" />
  </LoaderBoundary>
}

No more dispatching, no more handling null. Just use the data.

Concerns

  • the implementation of the task runner above contains an infinite loop. this implementation explains the concept but should definitely not be the implementation we move forward with
  • our current Loader (from skipper) allows us to wrap part of a component with a spinner but this pattern would force us to split up our components more because one component would stop rendering
  • These loader boundaries may unmount the component with useData calls if the fetch status becomes unknown suddenly. This may cause state to be reset too often.
  • React will ship real suspense for data fetching where we don't need to hack the error boundaries. I think these proposed APIs will be compatible with suspense for data fetching but will probably require some refactors when we switch to that.

Let me know what you think!

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    πŸ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. πŸ“ŠπŸ“ˆπŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❀️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.