Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

0.35 CRA-Demo Script and Feedback #5253

Closed
micahgodbolt opened this issue Feb 24, 2021 · 10 comments
Closed

0.35 CRA-Demo Script and Feedback #5253

micahgodbolt opened this issue Feb 24, 2021 · 10 comments
Assignees
Labels
area: dev experience Improving the experience of devs building on top of fluid area: examples Changes that focus on our examples design-required This issue requires design thought
Milestone

Comments

@micahgodbolt
Copy link
Member

micahgodbolt commented Feb 24, 2021

@fluid-example/cra-demo 0.35 release

Follow the tutorial then leave comments below

  • Were the steps clear?
  • What are your impressions about this approach?
  • What are the areas we need to improve?

We'll repeat this process with future releases

* This demo is a work in progress. There will be rough sections that need refactoring or refinement

@micahgodbolt micahgodbolt self-assigned this Feb 24, 2021
@micahgodbolt micahgodbolt changed the title CRA-Demo Script and Feedback 0.35 CRA-Demo Script and Feedback Feb 24, 2021
@micahgodbolt
Copy link
Member Author

micahgodbolt commented Feb 24, 2021


This demo is out of date. Please use the current demo found at aka.ms/fluid-cra


Demo introduction

In this demo you will be doing the following:

  1. Install Create-React-App with Typescript
  2. Install Fluid and Fluid Data Objects
  3. Import KVpair and Fluid Static
  4. Update the view
  5. Start a custom hook
  6. Loading the KVPair data object
  7. Syncing our app state with Fluid data
  8. Run the app!

1. Use Create-React-App with Typescript

npx create-react-app my-app-name --use-npm --template typescript
cd my-app-name

2. Install Fluid and Fluid Data Objects

npm install @fluid-experimental/fluid-static @fluid-experimental/data-objects

* These are still experimental packages, and not ready for production

Lastly, open up the App.tsx file, as that will be the only file we need to edit.

3. Import KVpair and Fluid Static

Fluid gives you access to methods to boostrap a new Fluid container and attach DataObjects to it.

KeyValueDataObject will provide you with a fully scaffolded distributed data structure to store "key value pair" data and subscribe to change events. The KeyValueInstantiationFactory is required by Fluid to instantiate the KeyValueDataObject.

// App.tsx
import { Fluid } from "@fluid-experimental/fluid-static";
import { KeyValueDataObject, KeyValueInstantiationFactory } from "@fluid-experimental/data-objects";

3.a Add the getContainerId function

The Fluid class helps you create or load a Fluid container. As you build your application, you'll eventually track these containers yourself. For now, getContainerId function either loads the container identified by the hash in the URL or creates a new container for you.

This is an area we'd like to improve, but, for now, paste this code below your imports.

// below imports
const getContainerId = (): { containerId: string; isNew: boolean } => {
    let isNew = false;
    if (window.location.hash.length === 0) {
        isNew = true;
        window.location.hash = Date.now().toString();
    }
    const containerId = window.location.hash.substring(1);
    return { containerId, isNew };
};

4. Update the view

In this simple multi-user app, we are going to build a button that, when pressed, shows the current time stamp. This allows co-authors to see the most recent timestamp at which any author pressed the button.

To start, remove all of the existing Create-React-App returned markup and replace it as shown below.

You can see that this UI requires a data object and setPair functions to work, so we'll add those above and pull them out of a function we need to write, called useKVPair. The plan is for data to be a simple JavaScript object, where setPair sets a key value pair on that object. This allows us to write out data.time once the value is set by the button click.

function App() {
  const [ data, setPair ] = useKVPair();

  if (!setPair) return <div />;

  return (
    <div className="App">
      <button onClick={() => setPair("time", Date.now().toString())}>
        click
      </button>
      <span>{data.time}</span>
    </div>
  );
}

5. Start a custom hook

Working in React, one of the best ways to abstract complex, reusable functionality is via a custom hook. Custom hooks are functions that have access to the built in React hooks like useState and useEffect which we'll need in order to load our Fluid DataObject, and track our local state.

Hooks are just functions with stateful return values. So our hook will return data of type KVData, and a method of type SetKVPair which will pulled in async.

These two returns are all that we'll need to build out our sample app. In more complex scenarios you might use a reducer pattern to pass down a set of dispatchable actions, rather than giving direct access to the SetKVPair.

// above function App()
type KVData = { [key: string]: any };
type SetKVPair = (key: string, value: any) => void;

function useKVPair(): [KVData, SetKVPair | undefined] {
    return [data, setPair]
};

6. Loading the KVPair data object

The first part of our hook will load the KVPair Data Object into a place where we can use all of its built in functionality. With the KVPair we'll be able to set data onto the Fluid data structure, listen for changes via the on method, and update our local app state anytime those changes occur. With a few lines of code we'll have a UI that reads, writes and reacts to incoming changes from this multi user application.

6.a Create a place to store our dataObject

Since we're working with async functions, we will need a place to store our KeyValueDataObject once it is loaded. This is why we're using React hooks, because inside of a hook, we can use React's useState to create a stateful value and a method to modify that state.

// inside useKVPair
const [dataObject, setDataObject] = React.useState<KeyValueDataObject>();

6.b Create/Load the Fluid document and data object

Now that we have a setter method, we need to make sure that our create/get flow runs just once, on app load. Here we'll use React's useEffect because it allows code to be ran as soon as the component loads, and re-run only when specific values change, which by setting the dependency array to [], means never.

The bulk of this useEffect hooks is the async load function that starts by getting or creating the fluidDocument. The FluidDocument class then allows use to get or create one or more data objects. This could be multiple KVPairs, or other DataObjects that you define.

// inside useKVPair
React.useEffect(() => {
    const { containerId, isNew } = getContainerId();

    const load = async () => {
        const fluidDocument = isNew
            ? await Fluid.createDocument(containerId, [KeyValueInstantiationFactory.registryEntry])
            : await Fluid.getDocument(containerId, [KeyValueInstantiationFactory.registryEntry]);

        const keyValueDataObject: KeyValueDataObject = isNew
            ? await fluidDocument.createDataObject(KeyValueInstantiationFactory.type, 'kvpairId')
            : await fluidDocument.getDataObject('kvpairId');

        setDataObject(keyValueDataObject);
    }

    load();

}, [])

Once the DataObject is returned we assign it to our dataObject state variable, and now we have access to all of the KVPair's methods, including set which we pass down as the setPair function by adding const setPair = dataObject?.set;

Here's our hook so far.

function useKVPair(): [KVData, SetKVPair | undefined] {
    const [dataObject, setDataObject] = React.useState<KeyValueDataObject>();

    React.useEffect(() => {
        const { containerId, isNew } = getContainerId();

        const load = async () => {...
        }

        load();

    }, [])

    const setPair = dataObject?.set;

    return [data, setPair];
}

7. Syncing our app state with Fluid data

It is possible to avoid syncing data between Fluid and app state, but for this demo we will have React state drive our UI updates, and sync Fluid data into our React state any time that Fluid data changes. The advantages of this approach are:

  1. We leverage React's ability to update its UI based on changing state (vs forcing a re-render).
  2. In the real world, React state will often be a subset of the entire Fluid data.
  3. An MVC/MVP approach will require Fluid data (as the app database) to be translated into queries passed into views anyway.

7.a Create a place to store our KVPair data

Just like in our dataObject example in step 6, we are going to use React's useState to store the data we sync in from Fluid. In our case, we're going to dump the entire store into state, but in real life examples this syncing would be selective based on if data pertinent to this view had changed.

This state is shaped like a normal JavaScript object (great for view frameworks), and we'll start with an empty default value so that we don't need to worry about an undefined state.

// inside useKVPair
const [data, setData] = React.useState<{ [key: string]: any }>({});

7.b Listen for changes and sync data

Setting up listeners is a common usecase for useEffect, and this is exactly what we're going to do. The main difference between this example and the one above (6.b) is that on first render we won't have access to the dataObject, and will need to wait for it to load. So we only set up our listener if the dataObject is defined, and we'll make sure the useEffect is fired any time that the dataObject changes (i.e. after it changes from undefined to defined).

// inside useKVPair
React.useEffect(() => {
    if (dataObject) {
        const updateData = () => setData(dataObject.query());
        updateData();
        dataObject.on("changed", updateData);
        return () => { dataObject.off("change", updateData) }
    }
}, [dataObject]);

The function we want called on load, and any time that Fluid data changes will be called updateData. This function syncs our React state, via setData, to our Fluid data. Here we can use the dataObject's query() method to return an object with all of the key value pairs stored in Fluid.

Lastly we return the off method to remove the listener as soon as this React view is removed.

8. A working application

To see this application working we first need to fire up a local Fluid server called Tinylicious

npx tinylicious

Then we're ready to start our React app

npm run start

They both happen to use port 3000, so CRA will ask if you want to use 3001. Just hit enter

When the app loads it will update the URL. Copy that new URL into a second browser and note that if you click the button in one browser, the other browser updates as well.

@ghost ghost added the triage label Feb 24, 2021
@skylerjokiel skylerjokiel added this to the March 2021 milestone Feb 24, 2021
@ghost ghost added triage and removed triage labels Feb 24, 2021
@curtisman curtisman added area: examples Changes that focus on our examples area: dev experience Improving the experience of devs building on top of fluid design-required This issue requires design thought and removed triage labels Feb 25, 2021
@DanWahlin
Copy link
Contributor

DanWahlin commented Mar 1, 2021

Nice and easy to go through. A few things to mention on the walk through:

  1. I like the approach. It'll be familiar and comfortable to React developers (at least those using hooks).
  2. Abstracting things out into the custom useKVPair() hook works well and should make it easier to port this to other front-end frameworks as well.
  3. I might be helpful right after they get the project going and install the dependencies to give a high-level overview of what they'll be doing so that when they perform each step they already have an idea in their mind of what they'll do. A visual that breaks things out into high-level objects might be helpful as well. This is super quick and off the top of my head, but something like this for the high-level overview:
  • Create a React app
  • Add a custom useKVPair() effect that does the following:
    • Handles loading a Fluid document and KeyValueDataObject used for real-time collaboration.
    • Provides a way to set data on the KeyValueDataObject and subscribe to changes made by collaborators.
  • Update the App component's JSX to add a button and display date information.
  • Start a Fluid server called Tinylicious.
  • Start the React app.
  1. It'd be helpful to add some basic comments (minimal) on key lines in the demo to explain what is happening. Pretty easy to follow for those who know something about Fluid but would be challenging for those new. I know it's early, but wanted to mention it. :-)
  2. The more you can have people add code in the order it appears the better. For example, const [data, setData] = React.useState<{ [key: string]: any }>({}); isn't added until later so for awhile I thought I missed adding something in the hook and went back to look. I'd have them add the shell for useKVPair() and then slowly add the code in the order it appears so that everything is there as they add things (explaining along the way like you do now). Maybe start with this and explain the key building blocks first:
function useKVPair(): any {
    // useState() here
    
    React.useEffect(() => {
    
    }, [])

    React.useEffect(() => {

    }, [dataObject]);
    
    // return object here

};

Then have them add the useState() calls:

const [dataObject, setDataObject] = React.useState<KeyValueDataObject>();
const [data, setData] = React.useState<{ [key: string]: any }>({});

Then have them fill in each useEffect(). And finally have them add the return value. By doing it that way they start really small, understand the basic building blocks, and then add to them over time. Just my two cents there.

  1. While I'm a big fan of custom types in "real" apps, for demos I think they tend to convolute things. For example, instead of having:
function useKVPair(): [KVData, SetKVPair | undefined] {
  ...
}

It might be better to just use any initially. I know....evil, but you could remove the custom type definitions and simplify the return signature initially which makes it way less intimidating.

Using more custom types is one of the things I've found that discourages people who may be new to those concepts. Especially those who come from a pure JavaScript background and haven't used TypeScript much. They tend to feel intimidated right upfront which ends up affecting everything they do in the walk through.

After they get it going you could add an optional step at the end to enhance the return signature of useKVPair(). Doing that lowers the barrier to entry and you could always mention that the any return type is there to keep it simple and that there will be an option at the end to enhance it with a custom type.

Comments on the code:

  1. I feel like there's an opportunity to cleanup the load() function potentially by having something like a createOrGetDocument() that takes isNew as a param. It just feels like duplication having createDocument() and getDocument() right next to each other even though they do different things of course. Same for getting the data object. Not a huge deal of course, but something that could really simplify things while still allowing people to use the create and get functions directly if they want.
  2. Not a fan of [KeyValueInstantiationFactory.registryEntry] but it sounded like that may change from one of our calls.

To wrap up, really nice job on this so don't let my comments above infer otherwise. They're just suggestions that I think can potentially take it to the "next level". I really like the direction this is going.

@reinvanleirsberghe
Copy link

reinvanleirsberghe commented Mar 4, 2021

Managed to get it work without hassle, so pretty clear for me...

Would like to see the same example using the @fluidframework/react though.
Just to see what's the difference...

Also is the KVPair suitable to sync/share large objects or should we use another DDS for that?
That's something that is not always clear for me.
Which DDS's you should use for different kind of datasets.

@micahgodbolt
Copy link
Member Author

@reinvanleirsberghe Thanks for the feedback! A few quick replies:

KVPair is pretty unrefined at the moment. It's meant to be a quick, simple DO you can pick up and use for those 80% of the cases where you simply need to sync a bit of data in an app

For larger data sets with more complicated APIs, I'd certainly recommend building your own purpose built DOs just like we did with KVPair. That process, and the challenge of picking the correct DDS for your type of data is beyond the scope of this demo, but certainly an were we hope to dig into in the future.

The goal of this demo is, as a react developer, how am I supposed to interact with ANY DataObject, and how can we make that experience the best it can be.

Thanks again for the feedback!

@LarsKemmann
Copy link
Contributor

Here's my feedback, by section, intentionally without having read any of the other comments or responses yet. (@SamBroner @ahmedbisht FYI.)

1)

  • Why is --use-npm included? Is this required? I'd prefer the tutorial documentation allow for both NPM and Yarn. NOTE: Some of my feedback, like this point, might seem like trivial issues, but every unexpected thing a user faces is going to be one more potential mental hurdle. For example, here you've already challenged a percentage of developers who are only familiar with Yarn and aren't sure if Yarn and NPM can both be used. I'm thinking about what you might publish to docs.microsoft.com, where you have the ability to show different views of the same code snippet in different "languages".

2)

  • Why are there two packages to install? Just let me install an all-in-one package and have treeshaking take care of the rest.

3)

  • You don't explain what a Fluid container is, or what a DataObject is. Not sure if that's supposed to be in scope here, but if this is the first link someone reads (and it likely will be, if this is the official "Get started with Fluid in React" documentation) then a brief explainer is needed.
  • I'm disappointed that there's still the distinction between a data object and its "instantiation factory". In my opinion, a 100-200 level user should not have to know about factories. If there is a very compelling value-add reason for why factories must be included and manually defined, then that value-add should be explained here and also clear from the API. No explanation is provided here, so now instead of introducing two concepts (KVpair and Fluid Static, like the section heading claims) you've required the user to learn three, and not provided any motivation for doing that.
  • "Lastly, open up the App.tsx file, as that will be the only file we need to edit." Okay, that makes me very happy. :)

3.a)

  • "The Fluid class helps you create or load a Fluid container. As you build your application, you'll eventually track these containers yourself." But why would I want to? It's not clear what any of that means at this point. See next suggestion.
  • What does this code do, and how is it related to the Fluid class? There's no mention of the Fluid class in the code. Having worked through other Fluid docs, I understand that what you're really doing here is just getting the current window's hash (or generating one if there isn't one already) and returning that as a container ID, but that isn't explained to the reader. Consider rewriting this section to say: "Fluid collaboration happens in containers, which have unique identifiers (like a document filename). For this example we'll use the hash part of the URL, and generate a new one if there isn't one present already."

4)

  • "In this simple multi-user app, we are going to build a button that, when pressed, shows the current time stamp. This allows co-authors to see the most recent timestamp at which any author pressed the button." --> Insert the following: "In this simple multi-user app, we are going to build a button that, when pressed, shows the current time stamp. We will store that timestamp using Fluid. This allows co-authors to see the most recent timestamp at which any author pressed the button."
  • "To start, remove all of the existing Create-React-App returned markup and replace it as shown below." Consider rewriting this to: "Replace the existing function App() {...} code block that was generated by Create-React-App with the following:". Then show the code snippet, and then the explanation paragraph after that.
  • "so we'll add those above" - I got lost at this point. Above where? Did I miss a code snippet? (Again, I figured out what you meant but I had to reread it three times.) Consider rewriting this paragraph as follows:
    "We are going to use a custom hook called useKVPair that provides a simple JavaScript object called data and a function called setPair. setPair sets a property on that data object. This allows us to write out data.time whenever the value is set or changed -- even by another user who is connected to the same Fluid container."
  • Why would setPair ever be falsy? Consider adding an inline code comment to the snippet as follows:
// Provide a fallback if collaboration has not been initialized yet in the background.
if (!setPair) return <div />;

5)

  • My first question is why this would have to be a custom hook at all. Can't Fluid provide useKVPair (or useFluidKey) out of the box, so I can just import it from an all-in-one package like @fluid-experimental/react? This seems like boilerplate/generic library code. I for one would love an API surface for Fluid React that requires nothing more of the developer than using some (library-provided) useFluid*** hooks to compose the different data object types. The hook function could accept an argument that helps provide the path of the data object within the container, right? Like useKVPair("documentMetadata.editInfo") which would then result in the container holding a shared tree (hierarchy/directory) with an entry for "documentMetadata", inside of which there's a shared map called "editInfo" which will hold a value under the "time" key once setPair(...) is called for the first time. Do I have the right mental model here of how this could work?
  • "Hooks are just functions with stateful return values." Props for one of the most concise definitions of hooks that I've come across. 😄 I really appreciate this being here, because you're bringing along the population of React developers who are not familiar with hooks.
  • Consider rewriting this section as follows, for clarity and conciseness:
    "Hooks in React are just functions with stateful return values, so changes to their return values cause a render update. They can be composed together into custom hooks, which are functions that have access to the built-in React hooks like useState and useEffect. Our custom hook will use these to load our shared Fluid data object and track our local state."
    Note that, since you haven't defined DataObject as a term yet, the revised paragraph doesn't use that formal term.

6)

  • "The first part of our hook will load the KVPair Data Object into a place where we can use all of its built in functionality." At this point I'm starting to feel reader fatigue/overwhelmed/uncertain. You need to give a bit of an outline of what steps we'll need to accomplish within the hook, maybe as code comments in the prior snippet. For example (I drafted this by skimming ahead briefly to try to figure out where this code is going):
function useKVPair(): [KVData, SetKVPair | undefined] {
  // TODO: Create a Fluid data object locally to store the key-value data
  // TODO: Create or load the Fluid document [NOTE: you used "container" up until now, so why the switch to "document"? One more mental hurdle for a reader to clear...]
  // TODO: Add the key-value data object to the document
  // TODO: Create a simple JavaScript key-value object that we'll pass to React
  // TODO: Listen for changes to the shared Fluid data object and update the state that we pass to React
  return [data, setPair]
};

NOTE: I have a feeling this will be one of the scariest things about this tutorial -- why on earth do we have to define the same data structure twice? This is probably also part of why I kept getting tripped up and failing to make it through understanding any of the previous Fluid React tutorials/documentation I've gone through. I can't tell you how many hours I've spent on trying to understand this issue and also why it's even an issue in the first place. :P
... And once again I question why this hook isn't provided by Fluid as the standard/recommended first-class API surface for React apps out of the box. This is a lot of work to expect from a developer and it seems like it's all boilerplate code taht the library should handle.

  • "With a few lines of code we'll have a UI that reads, writes and reacts to incoming changes from this multi user application." I disagree. I don't want to discourage the team or sound rude, but at this point, the user is most likely hearing: "With a lot of lines of boilerplate code we'll have a UI that reads, writes and reacts to incoming changes for only a single value. Good luck trying to scale this to a full-blown app."

6.a)

  • "Since we're working with async functions, we will need a place to store our KeyValueDataObject once it is loaded." This seems like a somewhat advanced (if not esoteric) concept in React Hooks. At this point the reader is expected to have not just a 100-200 level grasp of React but a 300-400 level grasp, which is jarring.

6.b)

  • "Now that we have a setter method, we need to make sure that our create/get flow runs just once, on app load." And Fluid can't handle this itself because...?
  • This code snippet introduces too many new concepts to list, but for starters - just in the API surface there's KeyValueInstantiationFactory, KeyValueInstantiationFactory.registryEntry, KeyValueInstantiationFactory.type, Fluid.createDocument, Fluid.getDocument, Fluid.createDataObject, fluidDocument.getDataObject, and worst of all the mysterious and unexplained 'kvpairId'. The user doesn't know, and can't reasonably be expected to learn, what all of those mean or how to compose them together. Again, this should be library code. (I'll try to stop repeating this point, but hopefully you're hearing how frustrating the experience is.) Maybe a 300-400 level user could be expected to learn or use these API surfaces, but not someone learning Fluid.
  • "Here's our hook so far." It's quite telling that the summary of what we've accomplished so far has to use ellipses to hide a bunch of code and make it seem less intimidating than it is.
  • Also, there's a sneaky insertion of a line of code here that you didn't actually instruct the user to add, I missed it even after reading this section three or four times & didn't spot it until step 7.b and wondering why the compiler was telling me setPair was undefined after all that work:
const setPair = dataObject?.set;

This should be included in the prior code snippet that tells the user what to insert. It's (evidently :)) too easy to gloss over the code snippet embedded in the short paragraph after the code snippet.

7)

  • I'm glad that some motivation is provided for the above approach here. However, in my mind it doesn't justify the extremely steep learning curve for a potential Fluid user (even for advanced React developers). Also, point Sample: type-race switch to use webpack-dev-server from prague-dev-server #3 is not explained so it just leaves more questions in the reader's mind than it answers.

7.a)

  • "in real life examples this syncing would be selective based on if data pertinent to this view had changed." I question how valid this is for most apps. Even if performance would be that much of an issue for medium-sized apps, it should be possible to scale up the API surface gradually. At this point you're basically admitting to the developer that the seemingly-generic useKVPair() hook is actually view-specific and that all this code will have to be rewritten for every single component in the app that uses Fluid. That's a non-starter.
  • Should this code be reusing the type definition that was created above?
const [data, setData] = React.useState<KVData>({});

7.b)

  • "The main difference between this example and the one above (6.b) is that on first render we won't have access to the dataObject, and will need to wait for it to load." At this point I was so done with trying to absorb concepts that I just started copy-pasting code without any understanding whatsoever, and figured I'd try to review on a second pass through once I at least had the code running.

8)

  • "They both happen to use port 3000, so CRA will ask if you want to use 3001. Just hit enter"

    One... more... mental hurdle... I cried a bit on the inside. Please fix the default ports in your tooling to not conflict with standard developer tooling.

Summary

2/5 stars, for at least being better than previous Fluid docs but still leaving me wishing I'd spent my Saturday differently. This even though I love the promise of Fluid and consider myself an expert-level developer/architect. I've actually built several real-time collaboration systems from scratch and did the equivalent of master's thesis research/work on the subject in a startup for two years. I'm going to keep at it because I'm extremely motivated, but unless Fluid provides more of its capabilities in a format that's at least somewhat adoptable by 200-level developers I can pretty much guarantee you it's not going to take off. I was hoping to be able to start an actual Fluid app today for a real-world business problem at our company (which my wife graciously gave me 5 hours of our weekend to do) and all I have to show for it is a single shared key-value in ~75 lines of code. 😢

@LarsKemmann
Copy link
Contributor

Now that I've read some of the other feedback/responses, I feel even more strongly about my 2/5 star review.

@micahgodbolt

The goal of this demo is, as a react developer, how am I supposed to interact with ANY DataObject, and how can we make that experience the best it can be.

I know that the changes I've asked for (like having a single @fluidframework/react NPM import with useFluidMap, useFluidTable, etc. hooks that are all I need to use those data structures) are not something that'll happen overnight, but I sincerely hope it gets prioritized before any kind of public preview/promotion (e.g. at Build). An API that requires as much boilerplate code as this one just to use a single shared time property is not adoption-ready.

I think part of the problem is that most of the Fluid examples I've come across, including this one, only ever show a single property, rather than an actual app. Maybe I'm just missing something and the APIs actually start to compose really well once you scale up from this point, but I don't see how that's possible.

Consider Redux, widely regarded as one of the most complex things you can do in React but also very widely adopted. The number of lines of code to create a shared model, reducers, etc. is 20 or less, and most of that is the user's data -- not using/composing framework APIs that the user has to learn. Once that is in place, it's ~5-10 lines of code to add a new property, reducer, etc.

Fluid is clocking in at 60+ LOC for a single shared value, most of which is highly intricate API composition that requires extreme attention to detail (there are some very nuanced things to consider in the hooks code in the sample above, as you call out in the supporting explanations), and there's no clear way to see that the next data object/type I want to add to my app won't also require that same level of effort since none of that code appears to be reusable in its current form.

@skylerjokiel
Copy link
Contributor

@LarsKemmann , thank you for taking the time to both go through our tutorial, and write such a thorough critique. We both need it and appreciate it greatly. Feedback is critical to our progress, and hopefully with iteration we can eventually arrive at least at a 4/5 star review. 😊

For context, this tutorial is a first step in a larger effort and hopefully I can provide a bit more framing around our thinking.

The Fluid Framework has a ton of raw power and is designed to be a base layer for building complex real-time applications. This power comes at the cost of complexity through abstractions. When developing the first versions of the framework one of our core principals was to provide the users (usually ourselves) the option to do whatever they wanted, wherever they wanted. This produced the foundation of the modern core framework and enables a lot of the complex (1000 level) scenarios we have within Microsoft. But abstraction comes at the price simplicity. When you can do everything you can't easily do anything.

We have historically been in the mindset that developers will build within some form of the Fluid ecosystem. There is a lot of benefits you get from building within the framework that we really want to bring to developers. This, "in the framework model," is apparent in our current HelloWorld experience that involves the developer building then consuming their DataObject. We've found, as you have, this is way to heavy for developers getting started. Lots of concepts to solve a simple problem like a dice roller. To make it easier for developer we are focusing on providing pre-built DataObjects that developers only have to consume. What you are seeing here is the initial attempt at providing this model of DataObject consumption.

This consumption model is a focused on the Fluid Framework being a Data+API layer in your webstack toolkit, and we have three primary principals that guide our decision making.

  1. Simple - Make it easy to use and simple to get started
  2. Embrace the web - Fluid needs to work with existing web technologies
  3. Depth is available - Provided clear paths to additional functionality

We are working to strike the balance is between simplicity for 100-200 level developers and depth for 300+ level developers. As you've pointed out we are still missing the mark on both and this is where community feedback and iteration is so critical. Your comments around reducing the number of concepts and specifically removing factory concepts is on our radar and something we will be addressing. Your comments around spending 5 hours and ending up with a single shared key-value in particular makes me very sad. We have support for alternate DataObjects and DataObject composition on our radar but your comment brings up other thoughts. We need to do a better job setting expectations for these types of engagements and specifically what we expect a developer to be able to do with the end product. This is definitely a simple 100 level learning tutorial with no path higher application and it should be represented that way.

The other difficult problem we are trying to solve for is scenario story telling from 100 to 200 to 300+ level developers. No developer will build an entire website with a collaborative dice roller but as begin to consider real applications we believe strongly that developers need to be able to bring web-technologies that work best for them. The tradeoffs here are similar to paragraph above but with a focus on ensuring someone who writes a 100 level application doesn't need to re-write their code to take advantage of depth within the framework. Considering the breadth of features the framework has, and the different points of integration, keeping the right amount of depth available to developers is a challenge.

Finally I want to talk about view frameworks. The idea of a @fluidframework/react package is something we've considered on multiple occasions. We actually have an existing package already that does this but has not been worked on in a while. As I mentioned above we've taken a strong focus on Fluid being the Data+API layer of the webstack. This is intentional because as a very small team working on developer experiences we believe that in order to make Fluid world class we need to first focus on the fundamentals. But, we also recognize that data in itself is not very exciting. We need to empower developers to build applications which means we need to embrace web technologies and integrate well with view frameworks. Kind of a chicken and egg problem we have here. The strong feedback I'm getting here is a need to understand the pieces and what is within your control as a developer. As an professional developer you should be able to walk away from this and say "I get how I could integrate this into my application." Instead you are asking why we even expose it. We cannot teach every scenario and we need to get to a state where developers can learn concepts that they can apply to their unique scenarios.

Finally, you've included a lot of pointed actionable feedback around both technical improvements and storytelling. I'll make sure these are included in our planning. If you feel particularly passionate about any issues please make sure your log an Issue.

Thanks again for taking time on your weekend. I've learned a lot and look forward to future engagements! 😊

@flvyu
Copy link
Contributor

flvyu commented Mar 9, 2021

@micahgodbolt Thanks for putting this tutorial together. I followed the tutorial and was able to get the app working. I think this is a great step towards making Fluid much easier to use and integrate in React apps.

Where the steps clear?
The steps were mostly clear.

I do think more information could be provided on why we were using certain Fluid APIs, methods, enums etc.

Overall, I think it is more important to focus on the Fluid API itself vs how the React APIs work.

For example, it would be great to add information on what createDocument does and what a document is modeling and abstracting. Without an explanation, this can lead to other questions like:

  1. Why do I need a document?
  2. What is the difference between a document and a container?
  3. What are the relationships between document and container?
  4. ...other stuff related to Fluid

Impression on Approach:
I really like how you used hooks to implement the logic for the data objects. This makes it much easier to add data sync logic in our apps and hopefully reduce the amount of code needed to get things working. The more we can abstract into hooks, the easier it will be to work with and use.

Other thoughts

  1. "Install Fluid..": I'm not sure what install Fluid means since I see we install the Fluid Static package. It might be clearer to explain what the Fluid Static Package is.
  2. In the end, it would great if we had a the final implementation of useKVPair in one place. This will allow us to double check we have the correct code by looking in one place instead of going through multiple sections. I did miss a line of code and had an error in my app. Similar issue that @LarsKemmann pointed out in step 7.a
  3. Since we use useEffect twice, it might be cleaner to add the import for it to the top.
  4. Does this have to be implemented in Typescript? It's only a little bit of TS, but new developers might be more familiar with regular JS.
  5. For me, I think Section 7 does a great job explaining the benefits of using React to sync the Fluid data.

For the point of this tutorial, I think the app we built makes sense, but to make Fluid more appealing I think a more interesting example would be more fun :)

I'm looking forward to using the experimental packages soon!

@reinvanleirsberghe
Copy link

Fluid.createDocument and Fluid.getDocument do not exists anymore as from version 0.36.0.
Updating the steps should be helpful :)

@micahgodbolt
Copy link
Member Author

@reinvanleirsberghe thanks for the reminder. Several things actually changed and my plan was to close this issue now that the 0.36 demo is out.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area: dev experience Improving the experience of devs building on top of fluid area: examples Changes that focus on our examples design-required This issue requires design thought
Projects
None yet
Development

No branches or pull requests

7 participants