Search code examples
reactjsrelayjsgraphql

RelayJS: Children components querying arbitrary data


I am building a dashboard that contains many different widgets. User can add and remove any widget and place them in any order they prefer to. Each widget has its own data requirement. What is the correct Relay way to construct the container hierarchy?

To provide some context this is the architecture so far:

Widget is a component that takes in a config object and render the corresponding component accordingly.

class Widget extends React.Component {
  render() {
    const {widget} = this.props;
    // widgetMap is a map that maps string to React component
    const ActualWidget = widgetMap.get(widget.component);

    return (
      <ActualWidget data={widget.data} />
    );
  }
}

export default Relay.createContainer(Widget, {
  fragments: {
    data: () => Relay.QL`
      fragment on ??? {
        # What should be here since we don't know what will be rendered?
      }
    `
  }
});

Dashboard component holds a number of widgets added by user.

class Dashboard extends React.Component {
  renderWidgets = () => {
    return this.props.widgets.map(widget => {
      return <Widget widget={widget}/>;
    });
  };

  render() {
    return (
      <div>
        <span>Hello, {this.props.user.name}</span>
        {this.renderWidgets()}
      </div>
    );
  }
}

export default Relay.createContainer(Dashboard, {
  fragments: {
    // `user` fragment is used by Dashboard
    user: () => Relay.QL`
      fragment on User {
        name
      }
    `,
    // Each widget have different fragment,
    // So what should be here?
  }
});

Update

I have tried to make each ActualWidget to be a field of viewer. So the schema is sort of like this:

type Viewer {
  widget1: Widget1
  widget2: Widget2
}

type Widget1 {
  name,
  fieldOnlyForWidget1
}

type Widget2 {
  name,
  fieldOnlyForWidget2
}

Then for my Widget container, I try to insert the fragment dynamically.

export default Relay.createContainer(Widget, {
  initialVariables: {
    component: 'Widget1' // Trying to set the string here
  }

  fragments: {
    data: (variables) => Relay.QL`
      fragment on Viewer { # We now know it is Viewer type
        # This didn't work because `variables.component` is not string! :(
        ${widgetMap.get(variables.component).getFragment('viewer')}
      }
    `
  }
});

That does not work. I believe Relay parsed the QL statically so it is unable to compose dynamic fragments. However that's just my guess.

I am in the process of testing the feasibility of using RootContainer for each widget and will update this soon.


Solution

  • I think the root issue here is you're trying to have the client decide, at runtime, the types of data to request based on some data given to it by the server (the widget names). That's always going to be a two-step process so you cannot nest the child fragments in the parent to do a single fetch; therefore, you'll need to set up a new RootContainer or something to kick off a new set of queries once you know what you need.

    However, I think you might be able pull this off as a single fetch using regular nesting by encoding the widget information in the graph itself. The trick here is to use GraphQL Union types: docs

    Union types should allow you to describe your "Dashboard" type as having a list of "Widgets". If you define "Widget" as a union type, you could have many different types of Widgets with their own unique fields and data.

    Once your server is serving up union types, you can then write a Dashboard relay container that looks something like this:

    var Dashboard = Relay.createContainer(DashboardComponent, {
      fragments: {
        dashboard: () => { console.log("dashboard query"); return Relay.QL`
          fragment on Dashboard {
            widgets {
              _typename
              ${FooWidget.getFragment("widget")}
              ${BarWidget.getFragment("widget")}
            }
          }
        `},
      }
    });
    

    Note the special "__typename" field that is defined on union types (see this question for some details). Your dashboard component can then use this.props.dashboard.widgets to iterate through all the widgets and create the appropriate widget based on the __typename, something like this:

    var widgets = this.props.dashboard.widgets.map((widget) => {
      if (widget.__typename == "FooWidget") {
        return <FooWidget widget={widget} />
      } else if (widget.__typename == "BarWidget") {
        return <CountdownWidget widget={widget} />
      }
    })
    

    I'm pretty sure this will work, but haven't tried it at all myself. From what I understand of Relay (which isn't that much), this would probably be the most "Relay-ish" way to model your problem.

    Does that make sense?