用 Relay 构建 Facebook 的 New Feed 应用

March 19, 2015 by Joseph Savona


在一月份的 React.js Conf 上,我们展示了一个 Relay 的预览版,它是一个新的在 React 中创建数据驱动应用的框架。这篇文章将会描述创建一个 Relay 应用的过程。这篇文章假设读者已经熟悉 Relay 和 GraphQL 的一些概念,因此如果你还不熟悉的话,建议先阅读我们的简述博客或者观看会议直播

我们为公开发布 GraphQL 和 Relay 而努力准备。与此同时,我们将会继续提供你期待的信息。


Relay 架构 #

下面的图片展示了在客户端和服务器端 Relay 架构的主要部分:

Relay 架构

主要部分如下:

  • Relay components: 反应数据的 React 组件。
  • Actions: 当用户操作的时候,相应的数据应该如何改变的描述。
  • Relay Store: 一个客户端的完全被框架管理的数据存储器。
  • Server: 一个带有 GraphQL 终端的 HTTP 服务器(一个用于读,一个用于写),用于响应 GraphQL 查询。

这篇文章将会集中在 Relay 组件 上,描述 UI 封装单元以及它们的数据依赖。这些组件构成了 Relay 应用的主要部分。


一个 Relay 应用 #

To see how components work and can be composed, let's implement a basic version of the Facebook News Feed in Relay. Our application will have two components: a <NewsFeed> that renders a list of <Story> items. We'll introduce the plain React version of each component first and then convert it to a Relay component. The goal is something like the following:

Sample News Feed


The <Story> Begins #

The first step is a React <Story> component that accepts a story prop with the story's text and author information. Note that all examples uses ES6 syntax and elide presentation details to focus on the pattern of data access.

// Story.react.js
class Story extends React.Component {
  render() {
    var story = this.props.story;
    return (
      <View>
        <Image uri={story.author.profile_picture.uri} />
        <Text>{story.author.name}</Text>
        <Text>{story.text}</Text>
      </View>
    );
  }
}

module.exports = Story;


What's the <Story>? #

Relay automates the process of fetching data for components by wrapping existing React components in Relay containers (themselves React components):

// Story.react.js
class Story extends React.Component { ... }

module.exports = Relay.createContainer(Story, {
  queries: {
    story: /* TODO */
  }
});

Before adding the GraphQL query, let's look at the component hierarchy this creates:

React Container Data Flow

Most props will be passed through from the container to the original component. However, Relay will return the query results for a prop whenever a query is defined. In this case we'll add a GraphQL query for story:

// Story.react.js
class Story extends React.Component { ... }

module.exports = Relay.createContainer(Story, {
  queries: {
    story: graphql`
      Story {
        author {
          name,
          profile_picture {
            uri
          }
        },
        text
      }
    `
  }
});

Queries use ES6 template literals tagged with the graphql function. Similar to how JSX transpiles to plain JavaScript objects and function calls, these template literals transpile to plain objects that describe queries. Note that the query's structure closely matches the object structure that we expected in <Story>'s render function.


<Story>s on Demand #

We can render a Relay component by providing Relay with the component (<Story>) and the ID of the data (a story ID). Given this information, Relay will first fetch the results of the query and then render() the component. The value of props.story will be a plain JavaScript object such as the following:

{
  author: {
    name: "Greg",
    profile_picture: {
      uri: "https://…"
    }
  },
  text: "The first Relay blog post is up…"
}

Relay guarantees that all data required to render a component will be available before it is rendered. This means that <Story> does not need to handle a loading state; the story is guaranteed to be available before render() is called. We have found that this invariant simplifies our application code and improves the user experience. Of course, Relay also has options to delay the fetching of some parts of our queries.

The diagram below shows how Relay containers make data available to our React components:

Relay Container Data Flow


<NewsFeed> Worthy #

Now that the <Story> is over we can continue with the <NewsFeed> component. Again, we'll start with a React version:

// NewsFeed.react.js
class NewsFeed extends React.Component {
  render() {
    var stories = this.props.viewer.stories; // `viewer` is the active user
    return (
      <View>
        {stories.map(story => <Story story={story} />)}
        <Button onClick={() => this.loadMore()}>Load More</Button>
      </View>
    );
  }

  loadMore() {
    // TODO: fetch more stories
  }
}

module.exports = NewsFeed;


All the News Fit to be Relayed #

<NewsFeed> has two new requirements: it composes <Story> and requests more data at runtime.

Just as React views can be nested, Relay queries can compose queries from child components. Composition in GraphQL uses ES6 template literal substitution: ${Component.getQuery('prop')}. Pagination can be accomplished with a query parameter, specified with <param> (as in stories(first: <count>)):

// NewsFeed.react.js
class NewsFeed extends React.Component { ... }

module.exports = Relay.createContainer(NewsFeed, {
  queryParams: {
    count: 3                             /* default to 3 stories */
  },
  queries: {
    viewer: graphql`
      Viewer {
        stories(first: <count>) {        /* fetch viewer's stories */
          edges {                        /* traverse the graph */
            node {
              ${Story.getQuery('story')} /* compose child query */
            }
          }
        }
      }
    `
  }
});

Whenever <NewsFeed> is rendered, Relay will recursively expand all the composed queries and fetch them in a single trip to the server. In this case, the text and author data will be fetched for each of the 3 story nodes.

Query parameters are available to components as props.queryParams and can be modified with props.setQueryParams(nextParams). We can use these to implement pagination:

// NewsFeed.react.js
class NewsFeed extends React.Component {
  render() { ... }

  loadMore() {
    // read current params
    var count = this.props.queryParams.count;
    // update params
    this.props.setQueryParams({
      count: count + 5
    });
  }
}

Now when loadMore() is called, Relay will send a GraphQL request for the additional five stories. When these stories are fetched, the component will re-render with the new stories available in props.viewer.stories and the updated count reflected in props.queryParams.count.


In Conclusion #

These two components form a solid core for our application. With the use of Relay containers and GraphQL queries, we've enabled the following benefits:

  • Automatic and efficient pre-fetching of data for an entire view hierarchy in a single network request.
  • Trivial pagination with automatic optimizations to fetch only the additional items.
  • View composition and reusability, so that <Story> can be used on its own or within <NewsFeed>, without any changes to either component.
  • Automatic subscriptions, so that components will re-render if their data changes. Unaffected components will not re-render unnecessarily.
  • Exactly zero lines of imperative data fetching logic. Relay takes full advantage of React's declarative component model.

But Relay has many more tricks up its sleeve. For example, it's built from the start to handle reads and writes, allowing for features like optimistic client updates with transactional rollback. Relay can also defer fetching select parts of queries, and it uses a local data store to avoid fetching the same data twice. These are all powerful features that we hope to explore in future posts.

React v0.13.1

March 16, 2015 by Paul O’Shannessy


距离我们发布 v0.13.0 已经过了不到一周的时间,但是是时候快速发布另外一个版本了。我们发布了 v0.13.1,修复了一系列小的问题。

感谢在应用中升级了 React 的开发者们,耗费时间来提出问题。同时,非常感谢为这些发现的问题提交 pull request 的开发者们!今天的已修复的六个问题当中的两个问题被来自于非核心团队的人员修复!

该版本现在可以下载了:

我们也在 npm 上发布了 0.13.1Reactreact-tools 包,在 bower 上发布了 React 包。


更新日志 #

React 核心代码 #

Bug 修复 #

  • 当渲染空的 <select> 元素的时候不抛出异常
  • 当传入 null 的时候,确保更新 style 能正常工作

带有附加组件的 React #

Bug 修复 #

  • TestUtils: 在 ES6 类上调用 getDOMNode 的时候不输出警告
  • TestUtils:确保包裹整个页面的组件(<html>, <head>, <body>)被当做 DOM 组件
  • Perf:停止重复计算 DOM 组件

React 工具 #

Bug 修复 #

  • 修复 --non-strict-es6module 可选项解析

React v0.13

March 10, 2015 by Ben Alpert


今天,我们很高兴发布了 React v0.13!

该版本最重要的特性就是 支持 ES6 类,让开发者开发组件的时候更加灵活。我们最终的目标是用 ES6 类完全取代 React.createClass,但是在语言中有当前的混入( mixin )和类属性初始化器的替代解决方案之前,我们不打算反对使用 React.createClass

在上周的 EmberConf 和 ng-conf 上,很振奋地看到 Ember 和 Angular 已经着手优化运行速度,现在两者的表现都可以与 React 媲美。我们一直认为性能表现并不是选择 React 的最重要原因,但是我们仍然计划做更多优化来使 React 更加快速

我们计划的优化方案需要 ReactElement 是不可变的,这也是一个写标准 React 代码的最佳实践。在这个版本中,我们增加了运行时警告,这些警告在元素被渲染创建中 props 被改变或者添加的时候触发。在移植代码的时候,可能想使用新的 React.cloneElement API(该方法和 React.addons.cloneWithProps 类似,除了维护 keyref,并且不自动合并 styleclassName 之外)。更多关于我们计划的优化,参见 Gitub issues #3226, #3227, #3228

该版本现在可以下载了:

我们也在 npm 上发布了 0.13.0Reactreact-tools 包,在 bower 上发布了 React 包。


更新日志 #

React 核心代码 #

破坏性的改变( Breaking Changes ) #

  • 在 0.12 中不建议使用的东西已经无法正常工作了:最重要的是,调用组件类的时候不使用 JSX 也不使用 React.createElement,而是在 JSX 中使用非组件函数或者 createElement
  • 不建议在元素创建之后改变 props,并且在开发模式下会出现警告;React 的未来版本将会认为 props 是不可变的,从而提高性能
  • 静态方法(在 statics 中定义的方法)不再自动绑定到组件类
  • ref 绑定顺序做了一些微调,比如指向某个组件的 ref 在它的 componentDidMount 方法调用之后就马上可以使用了;这个改变仅在某个子组件的 compoenentDirMount 内调用父组件的回调函数,这是一种反模式(anti-pattern),应该尽量规避。
  • 现在,在生命周期方法中调用 setState 总是会被批量处理(译者注:也就是说在某个生命周期函数中调用了多次 setState,这些 setState 的产生的改变会集中在一起处理,比如计算最小的 DOM 改变等),因此该方法是异步的。在组件第一次挂载并且该函数第一次被调用的时候,该函数是同步的。
  • 现在,在未挂载的组件上调用 setStateforceUpdate 将会出现警告信息,而不再是抛出异常。这就在使用 Promises 的时候避免了可能的竞争条件(That avoids a possible race condition with Promises)。
  • 已经不能访问大多数内置的属性了,包括 this._pendingStatethis._rootNodeID

新特性 #

  • 支持使用 ES6 类来创建 React 组件;详细内容参考 v0.13.0 beta 1 notes
  • 添加新的顶层 API React.findDOMNode(compoment),这应该用于代替 component.getDOMNode()。基于 ES6 组件的基类将不会有 getDOMNode 方法。这一改变将会使更多的机制(patterns)进化变为可能。
  • 添加了新的顶层 API React.cloneElement(el, props) 来制作 React 元素副本 - 详细内容参考 v0.13 RC2 notes
  • 新的 ref 风格,允许使用回调函数而不仅仅是一个名字字符串:<Photo ref={(c) => this._photo = c} /> 允许用 this._photo 指向组件(而不是通过 ref="photo" 绑定在 this.refs.photo)。
  • this.setState() 现在能够用一个函数作为第一个参数,用于转换 state 更新(for transactional state updates),比如,this.setState((state, props) => ({count: state.count + 1})); - 这意味着再也不需要使用 this._pendingState 了,并且该属性现在也被删除了。
  • 支持迭代器和 immutable-js(Support for iterators and immutable-js sequences as children)。

不建议使用的 #

  • ComponentClass.type 不建议使用。使用 ComponentClass (通常这样使用:element.type === ComponentClass)。
  • 一些在基于 createClass 创建组件的方法在 ES6 类中被移除掉了或者不建议使用了(getDOMNode, replaceState, isMounted, setProps, replaceProps)。

带有附加组件的 React #

新特性 #

不建议使用的 #

  • React.addons.classSet 现在不建议使用了。这个功能可以被几个能自由使用的模块替换。classnames 就是一个这样的模块。
  • React.addons.cloneWithProps 可以用 React.cloneElement 替换 - 如果需要,确保手动合并 styleclassName

React 工具 #

破坏性的改变(Breaking Changes) #

  • ES6 语法转换的时候, 方法默认情况下不再可枚举,这需要 Object.defineProperty;如果要支持像 IE8 这种浏览器,可以带上 --target es3 来模仿旧式的行为

新特性 #

  • --target 可选项在 jsx 命令里可用了,允许用户指定要转换成的 ECMAScript 语言版本。
    • es5 是默认的。
    • es3 修正了前面的默认行为。这里添加了额外的转换,以确保用保留字作为属性是安全的(例如,this.static 将会转换成 this['static'] 来兼容 IE8)。
  • 省略号操作符(spread operator)转换仍然被支持。

JSX #

破坏性的改变(Breaking Changes) #

  • 关于某些 JSX 如何解析有一点改变,尤其是在某个元素中使用 >}。之前这被当做一个字符串,但是现在将会是一个解析错误。npm 上的 jsx_orphaned_brackets_transformer 包可以用来找出并修复 JSX 代码中潜在的问题。

React v0.13.0 Beta 1

January 27, 2015 by Sebastian Markbåge


React 0.13 has a lot of nice features but there is one particular feature that I'm really excited about. I couldn't wait for React.js Conf to start tomorrow morning.

Maybe you're like me and staying up late excited about the conference, or maybe you weren't one of the lucky ones to get a ticket. Either way I figured I'd give you all something to play with until then.

We just published a beta version of React v0.13.0 to npm! You can install it with npm install react@0.13.0-beta.1. Since this is a pre-release, we don't have proper release notes ready.

So what is that one feature I'm so excited about that I just couldn't wait to share?

Plain JavaScript Classes!! #

JavaScript originally didn't have a built-in class system. Every popular framework built their own, and so did we. This means that you have a learn slightly different semantics for each framework.

We figured that we're not in the business of designing a class system. We just want to use whatever is the idiomatic JavaScript way of creating classes.

In React 0.13.0 you no longer need to use React.createClass to create React components. If you have a transpiler you can use ES6 classes today. You can use the transpiler we ship with react-tools by making use of the harmony option: jsx --harmony.

ES6 Classes #

class HelloMessage extends React.Component {
  render() {
    return <div>Hello {this.props.name}</div>;
  }
}

React.render(<HelloMessage name="Sebastian" />, mountNode);

The API is mostly what you would expect, with the exception for getInitialState. We figured that the idiomatic way to specify class state is to just use a simple instance property. Likewise getDefaultProps and propTypes are really just properties on the constructor.

export class Counter extends React.Component {
  constructor(props) {
    super(props);
    this.state = {count: props.initialCount};
  }
  tick() {
    this.setState({count: this.state.count + 1});
  }
  render() {
    return (
      <div onClick={this.tick.bind(this)}>
        Clicks: {this.state.count}
      </div>
    );
  }
}
Counter.propTypes = { initialCount: React.PropTypes.number };
Counter.defaultProps = { initialCount: 0 };

ES7+ Property Initializers #

Wait, assigning to properties seems like a very imperative way of defining classes! You're right, however, we designed it this way because it's idiomatic. We fully expect a more declarative syntax for property initialization to arrive in future version of JavaScript. It might look something like this:

// Future Version
export class Counter extends React.Component {
  static propTypes = { initialCount: React.PropTypes.number };
  static defaultProps = { initialCount: 0 };
  state = { count: this.props.initialCount };
  tick() {
    this.setState({ count: this.state.count + 1 });
  }
  render() {
    return (
      <div onClick={this.tick.bind(this)}>
        Clicks: {this.state.count}
      </div>
    );
  }
}

This was inspired by TypeScript's property initializers.

Autobinding #

React.createClass has a built-in magic feature that bound all methods to this automatically for you. This can be a little confusing for JavaScript developers that are not used to this feature in other classes, or it can be confusing when they move from React to other classes.

Therefore we decided not to have this built-in into React's class model. You can still explicitly prebind methods in your constructor if you want.

class Counter extends React.Component {
  constructor() {
    super();
    this.tick = this.tick.bind(this);
  }
  tick() {
    ...
  }
  ...
}

However, when we have the future property initializers, there is a neat trick that you can use to accomplish this syntactically:

class Counter extends React.Component {
  tick = () => {
    ...
  }
  ...
}

Mixins #

Unfortunately, we will not launch any mixin support for ES6 classes in React. That would defeat the purpose of only using idiomatic JavaScript concepts.

There is no standard and universal way to define mixins in JavaScript. In fact, several features to support mixins were dropped from ES6 today. There are a lot of libraries with different semantics. We think that there should be one way of defining mixins that you can use for any JavaScript class. React just making another doesn't help that effort.

Therefore, we will keep working with the larger JS community to create a standard for mixins. We will also start designing a new compositional API that will help make common tasks easier to do without mixins. E.g. first-class subscriptions to any kind of Flux store.

Luckily, if you want to keep using mixins, you can just keep using React.createClass.

Note:

The classic React.createClass style of creating classes will continue to work just fine.

Other Languages! #

Since these classes are just plain old JavaScript classes, you can use other languages that compile to JavaScript classes, such as TypeScript.

You can also use CoffeeScript classes:

div = React.createFactory 'div'

class Counter extends React.Component
  @propTypes =
    initialCount: React.PropTypes.number
  @defaultProps =
    initialCount: 0
  constructor: ->
    @state =
      count: @props.initialCount
  tick: =>
    @setState count: @state.count + 1
  render: ->
    div(onClick: @tick, 'Clicks: ', @state.count)

You can even use the old ES3 module pattern if you want:

function MyComponent(initialProps) {
  return {
    state: { value: initialProps.initialValue },
    render: function() {
      return <span className={this.state.value} />
    }
  };
}

React.js Conf Diversity Scholarship

December 19, 2014 by Paul O’Shannessy


Today I'm really happy to announce the React.js Conf Diversity Scholarship! We believe that a diverse set of viewpoints and opinions is really important to build a thriving community. In an ideal world, every part of the tech community would be made up of people from all walks of life. However the reality is that we must be proactive and make an effort to make sure everybody has a voice. As conference organizers we worked closely with the Diversity Team here at Facebook to set aside 10 tickets and provide a scholarship. 10 tickets may not be many in the grand scheme but we really believe that this will have a positive impact on the discussions we have at the conference.

I'm really excited about this and I hope you are too! The full announcement is below:


The Diversity Team at Facebook is excited to announce that we are now accepting applications for the React.js Conf scholarship!

Beginning today, those studying or working in computer science or a related field can apply for an all-expense paid scholarship to attend the React.js Conf at Facebook’s Headquarters in Menlo Park, CA on January 28 & 29, 2015. React opens a world of new possibilities such as server-side rendering, real-time updates, different rendering targets like SVG and canvas. Join us at React.js Conf to shape the future of client-side applications! For more information about the React.js conference, please see the website and previous updates on our blog.

At Facebook, we believe that anyone anywhere can make a positive impact by developing products to make the world more open and connected to the people and things they care about. Given the current realities of the tech industry and the lack of representation of communities we seek to serve, applicants currently under-represented in Computer Science and related fields are strongly encouraged to apply. Facebook will make determinations on scholarship recipients in its sole discretion. Facebook complies with all equal opportunity laws.

To apply for the scholarship, please visit the Application Page: https://www.surveymonkey.com/s/XVJGK6R

Award Includes #

  • Paid registration fee for the React.js Conf January 28 & 29th at Facebook’s Headquarters in Menlo Park, CA
  • Paid travel and lodging expenses
  • Additional $200 meal stipend

Important Dates #

  • Monday, January 5, 2015: Applications for the React.js Conf Scholarship must be submitted in full
  • Friday, January 9, 2015: Award recipients will be notified by email of their acceptance
  • Wednesday & Thursday, January 28 & 29, 2015: React.js Conf

Eligibility #

  • Must currently be studying or working in Computer Science or a related field
  • International applicants are welcome, but you will be responsible for securing your own visa to attend the conference
  • You must be available to attend the full duration of React.js conf on January 28 and 29 at Facebook Headquarters in Menlo Park