Skip to content

组件设计模式(3):render props 模式 ​

作者:Long Mo
字数统计:1.6k 字
阅读时长:5 分钟

在上一小节中,我们介绍了高阶组件,高阶组件并不是 React 中唯一的重用组件逻辑的方式, 在这一小节中,我们会介绍另一种方式 render props。

render props ​

所谓 render props,指的是让 React 组件的 props 支持函数这种模式。因为作为 props 传入的函数往往被用来渲染一部分界面,所以这种模式被称为 render props。

一个最简单的 render props 组件 RenderAll,代码如下:

jsx
const RenderAll = (props) => {
	return (
		<React.Fragment>
			{props.children(props)}
		</React.Fragment>
	);
};

这个 RenderAll 预期子组件是一个函数,它所做的事情就是把子组件当做函数调用,调用参数就是传入的 props,然后把返回结果渲染出来,除此之外什么事情都没有做。

使用 RenderAll 的代码如下:

jsx
<RenderAll>
	{() => <h1>hello world</h1>}
</RenderAll>

可以看到,RenderAll 的子组件,也就是夹在 RenderAll 标签之间的部分,其实是一个函数。这个函数渲染出

hello world

,这就是上面使用 RenderAll 渲染出来的结果。

当然,这个 RenderAll 没做任何实际工作,接下来我们看 render props 真正强悍的使用方法。

传递 props ​

和高阶组件一样,render props 可以做很多的定制功能,我们还是以根据是否登录状态来显示一些界面元素为例,来实现一个 render props。

下面是实现 render props 的 Login 组件,可以看到,render props 和高阶组件的第一个区别,就是 render props 是真正的 React 组件,而不是一个返回 React 组件的函数。

jsx
const Login = (props) => {
	const userName = getUserName();

	if (userName) {
		const allProps = { userName, ...props };
		return (
			<React.Fragment>
				{props.children(allProps)}
			</React.Fragment>
		);
	} else {
		return null;
	}
};

当用户处于登录状态,getUserName 返回当前用户名,否则返回空,然后我们根据这个结果决定是否渲染 props.children 返回的结果。

当然,render props 完全可以决定哪些 props 可以传递给 props.children, 在 Login 中,我们把 userName 作为增加的 props 传递给下去,这样就是 Login 的增强功能。

一个使用上面 Login 的 JSX 代码示例如下:

jsx
<Login>
	{({ userName }) => <h1>Hello {userName}</h1>}
</Login>

对于名为“程墨Morgan”的用户登录,上面的 JSX 会产生

Hello 程墨Morgan

。

不局限于 children ​

在上面的例子中,作为 render 方法的 props 就是 children, 在我写的《深入浅出React和Redux》中,将这种模式称为“以函数为子组件(function as child)”的模式, 这可以算是 render props 的一种具体形式,也就利用 children 这个 props 来作为函数传递。

实际上,render props 这个模式不必局限于 children 这一个 props,任何一个 props 都可以作为函数,也可以利用多个 props 来作为函数。

我们来扩展 Login,不光在用户登录时显示一些东西,也可以定制用户没有登录时显示的东西,我们把这个组件叫做 Auth,对应代码如下:

jsx
const Auth = (props) => {
	const userName = getUserName();

	if (userName) {
		const allProps = { userName, ...props };
		return (
			<React.Fragment>
				{props.login(allProps)}
			</React.Fragment>
		);
	} else {
		<React.Fragment>
			{props.nologin(props)}
		</React.Fragment>
	}
};

使用 Auth 的话,可以分别通过 login 和 nologin 两个 props 来指定用户登录或者没登录时显示什么,用法如下:

jsx
<Auth
	login={({ userName }) => <h1>Hello {userName}</h1>}
	nologin={() => <h1>Please login</h1>}
/>

依赖注入 ​

render props 其实就是 React 世界中的 “依赖注入”(Dependency Injection)。

所谓依赖注入,指的是解决这样一个问题:逻辑 A 依赖于逻辑 B,如果让 A 直接依赖于 B,当然可行,但是 A 就没法做得通用了。依赖注入就是把 B 的逻辑以函数形式传递给 A,A 和 B 之间只需要对这个函数接口达成一致就行,如此一来,再来一个逻辑 C,也可以用一样的方法重用逻辑 A。

在上面的代码示例中,Login 和 Auth 组件就是上面所说的逻辑 A,而传递给组件的函数类型 props,就是逻辑 B 和 C。

render props 和高阶组件的比较 ​

我们来比对一下这两种重用 React 组件逻辑的模式。

首先,render props 模式的应用,就是做一个 React 组件,而高阶组件,虽然名为“组件”,其实只是一个产生 React 组件的函数。

render props 不像上一小节中介绍的高阶组件有那么多毛病,如果说 render props 有什么缺点,那就是 render props 不能像高阶组件那样链式调用,当然,这并不是一个致命缺点。

render props 相对于高阶组件还有一个显著优势,就是对于新增的 props 更加灵活。 还是以登录状态为例,假如我们扩展 withLogin 的功能,让它给被包裹的组件传递用户名这个 props,代码如下:

jsx
const withLogin = (Component) => {
	const NewComponent = (props) => {
		const userName = getUserName();
		if (userName) {
			return <Component {...props} userName={userName} />;
		} else {
			return null;
		}
	}

	return NewComponent;
};

这就要求被 withLogin 包住的组件要接受 userName 这个props。可是,假如有一个现成的 React 组件不接受 userName,却接受名为 name 的 props 作为用户名,这就麻烦了。我们就不能直接用 withLogin 包住这个 React 组件,还要再造一个组件来做 userName 到 name 的映射,十分费事。

对于应用 render props 的 Login,就不存在这个问题,接受 name 不接受 userName 是吗?这样写就好了:

jsx
<Login>
	{
		(props) => {
			const { userName } = props;
			return <TheComponent {...props} name={userName} />
		}
	}
</Login>

所以,当需要重用 React 组件的逻辑时,建议首先看这个功能是否可以抽象为一个简单的组件;如果行不通的话,考虑是否可以应用 render props 模式;再不行的话,才考虑应用高阶组件模式。

这并不表示高阶组件无用武之地,在后续章节,我们会对 render props 和高阶组件分别讲解具体的实例。

小结 ​

在这一小节中,我们介绍了 render props 这种模式,也将 render props 和高阶组件两种模式进行了比较。

读者应该要明白:

  • render props 的形式;
  • render props 其实就是“依赖注入”;
  • 如何利用 render props 实现共享组件之间的逻辑。

Contributors ​

Long Mo
文章作者:Long Mo
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Longmo Docs !