设计React的10种模式
本文翻译自10 React mini-patterns。这篇文章由mrcode翻译, 如果哪里翻译的不恰当或有错误的地方,欢迎指出。 同时也希望大家关注我的博客。 关注我的账号。
在过去的几年里,我已经做了许多看起来挺不错的React项目。
在这个神奇的旅程中,一些模式出现过很多次,我发现我一次又一次地重复着这些模式。
什么是模式?
这些模式是我想在学习React第一天就知道的事情。
所以如果今天是你第一天学习React,你是如此的幸运。
或者你并不幸运。只有一种方法可以决定你是否是幸运的...
这是一个长长的列表,所以你可以跳过无聊的一些模式, 比如:3,6,8,10。
1. Sending data down and up
我建议大家新学习React的一件事是传递信息的模式(信息可以是对象,字符串等)和传递方法下来允许子组件传递信息给父组件。
就像把一包芯片和一个对讲机送到地下被困的矿工一样。
图片怎么样?
下面的事情是这种模式的最简单的形式。
父组件在左边,子组件在右边。
你可以认为连接这些组件的两个props允许信息在两者之间的任一方向上流动。
被称为items
将被传递给子组件, deleteItem
将提供给子组件一种方案来发送信息给父组件。
这不是一个真正的模式。剩下的肯定都是模式。我承诺。
2. Fixing HTML’s inputs
React和web组件的一个伟大的事情是,如果在html中的东西不能按你想要的方式工作,你可以解决它。
如果你考虑允许用户输入的不同元素,你很快就会看到这些元素的命名是荒谬的,几乎是鲁莽的。
如果我建立一个将有很多用户输入的网站,我做的第一件事之一是解决这个问题。
还有更多的改进:
输入应该通过onChange方法返回一个值,而不是一个JavaScript事件实例,对不?
你可以进一步确保在onChange返回的数据类型和传递的数据类型相匹配。如果
typeof props.value
是number,然后将e.target.value
回到一个数字,然后再次发出数据。一组单选按钮在功能上与
<select>
一样
它是搞砸了,以一种完全不同的方式来对待它们,唯一的区别是UI。
也许你的应用程序有一个单一的<PickOneFromMany />
组件,并传递ui =“radio”
或ui =“dropDown”
。
关键是不要像我这样做。
关键是要使它们成为你自己的 - 你不需要继续使用HTML的用户输入元素的坑爹性质。
3. Binding labels to inputs with unique IDs
关于输入...如果你关心你的用户,你将通过id / for组合将<label>
元素绑定到你的<input>
。
但是你不想为你定义的每个输入想出一些聪明和独特的id,谁有时间呢?我不知道你,但我有山羊的视频观看。
(提示:如果您的航班上有一个尖叫的孩子,闭上眼睛,假装您在YouTube上观看的山羊听起来像人类的视频,烦人的声音就会变得很热闹。)
您可以为每个输入/标签对生成随机ID,但是客户端呈现的HTML将与您呈现的服务器呈现的HTML不匹配。这并不是一个好的解决方案
所以,你可以创建一个小的模块,给出一个递增的ID,并在输入组件中使用它,像这样:
class Input extends React.Component { constructor(props) { super(props); this.id = getNextId(); this.onChange = this.onChange.bind(this); } onChange(e) { this.props.onChange(e.target.value); } render() { return ( <label htmlFor={this.id}> {this.props.label} <input id={this.id} value={this.props.value} onChange={this.onChange} /> </label> ); } }
如果getNextId()
每次只是增加一个数字,然后在服务器上渲染时,这个数字会继续上升和起来,最终达到无穷大。因此,您需要在每次呈现应用程序时重置该数字(对于每个网络请求)。
你可以在你的应用程序的入口点,使用一个简单的resetId()或任何你认为最好的名称。
考虑到所有这些,你的超级幻想模块可能看起来像这样:
let count = 1; export const resetId = () => { count = 1; } export const getNextId = () => { return `element-id-${count++}`; }
4. Controlling CSS with props
当你想在不同的实例(例如'primary'和'secondary'按钮)应用不同的CSS,你可以传递道具来控制要应用的CSS。
这看起来超级简单的表面,但让我向你保证有很多错误的方法来做到这一点(我已经尝试过他们!)。
有 - 我估计 - 三种不同的方式,你可以控制应用于组件的CSS。
使用标志
也许你的一些按钮有圆角,但这不直接对应于您定义的主题。
在这种情况下,你可以坐下你的设计师,并有一致性谈话,或创建一个布尔的道具,可能看起来像这样:
<Button theme =“secondary”rounded> Hello </ Button>
就像HTML的二进制属性一样,你不需要做round = {true}
。
设置值
在某些情况下,您可能希望直接传递CSS属性的值(在组件中将其设置为内联样式)。
<Icon width =“25”height =“25”type =“search”/>
一个例子
假设您正在创建链接组件。你通过你的网站的设计和工作,有三个不同的主题,有时他们有一个下划线,有时他们不。
下面是我将如何设计该组件:
const Link = (props) => { let className = `link link--${props.theme}-theme`; if (!props.underline) className += ' link--no-underline'; return <a href={props.href} className={className}>{props.children}</a>; }; Link.propTypes = { theme: PropTypes.oneOf([ 'default',// primary color,no underline 'blend',// inherit surrounding styles 'primary-button',solid block ]),underline: PropTypes.bool,href: PropTypes.string.isrequired,children: PropTypes.oneOfType([ PropTypes.element,PropTypes.array,PropTypes.string,]).isrequired,}; Link.defaultProps = { theme: 'default',underline: false,};
增加CSS...
.link--default-theme,.link--blend-theme:hover { color: #D84315; } .link--blend-theme { color: inherit; } .link--default-theme:hover,.link--blend-theme:hover { text-decoration: underline; } .link--primary-button-theme { display: inline-block; padding: 12px 25px; font-size: 18px; background: #D84315; color: white; } .link--no-underline { text-decoration: none; }
你可能已经注意到链接 - 无下划线的选择器是没必要存在的, 因为他双重否定了。
故事时间:我曾经认为写CSS更少的CSS是目标,但它不是。我宁愿有一些双重否定和多选择器规则集,如果它的意思是样式以一个很好的分层方式应用的话。
我相信我以前说过,但缩放网站最困难的事情是CSS。 JavaScript很容易,但是随意使用CSS使你很遭罪 - 一旦你开始混乱,这是不容易中途修改来解决的。
真实的事实:CSS的特异性是网络开发人员死亡的第一原因。如果你在一台大型计算机上,请查看顶部导航栏中的小通知图标的CSS。
这个通知图标是由很多CSS样式组合在一起的。很复杂。
二十三条规则。
这不包括继承自十一个其他规则的样式。行高单独被覆盖九次。
如果line-height是一只猫,它现在已经死了。
这不能令人愉快地维护。
有了React,我们可以做得更好。我们可以仔细设计哪些类应用于我们的组件。我们可以删除全局样式和移动它所有在我们的Button.scss。我们可以消除对文件的特异性和顺序的所有依赖。
附注: 我梦想着有一天游览器对于样式没有自己的看法(意思就是所有游览器都变得统一, 完全去IE化-。-)。
5. The switching component
切换组件是呈现最多的组件之一。
这可能是一个显示多个页面之一的<Page>组件。或选项卡集中的选项卡,或模态组件中的不同模态。
我曾经使用switch语句,进一步到实际传入我想要渲染的组件。然后从组件本身导出对组件的引用(命名为exports,然后作为组件上的属性)。
真是一堆可怕的想法!
我现在的方法是使用一个对象传递props给Page组件。
import HomePage from './HomePage.jsx'; import AboutPage from './AboutPage.jsx'; import UserPage from './UserPage.jsx'; import FourOhFourPage from './FourOhFourPage.jsx'; const PAGES = { home: HomePage,about: AboutPage,user: UserPage,}; const Page = (props) => { const Handler = PAGES[props.page] || FourOhFourPage; return <Handler {...props} /> }; Page.propTypes = { page: PropTypes.oneOf(Object.keys(PAGES)).isrequired,};
PAGES对象的key可以在prop类型中使用,以捕获dev时间错误。
然后,我们当然会使用这样<page page =“home”/>。
如果你用key替换home,about和user分别用/, /about和/user,你差不多就是个路由器了。
(未来的想法:再见 react-router。)
6. Reaching into a component
如果您正在寻找一个简单的方法来请求您的用户输入信息,那么你可以添加自动对焦到输入组件, 当用户一个页面的时候。这种设计仅仅适用于登陆操作就在主页面内执行, 而不是单独弹出一个模态窗口。
你可以通过给输入组件一个id,然后使用document.getElementById('user-name-input')。focus()
来将用户的焦点集中在输入组件上。
这工作,但不是正确的方式。在你的应用程序中依靠两个字符串匹配的事情越少越好。
这可以正常的工作, 但确不是最好的方式。 在你的代码中依靠两个字符串匹配的事情越少越好。
幸运的是,有一个非常容易的方法来做到这一点“正确”:
class Input extends Component { focus() { this.el.focus(); } render() { return ( <input ref={el=> { this.el = el; }} /> ); } }
真是酷炫屌炸天! 一个具有focus()方法的输入组件,用于聚焦HTML元素。
在父组件中,我们可以获得对Input组件的引用并调用其focus()方法。
class SignInModal extends Component { componentDidMount() { this.InputComponent.focus(); } render() { return ( <div> <label>User name: </label> <Input ref={comp => { this.InputComponent = comp; }} /> </div> ) } }
注意,当在组件上使用ref时,它是对组件(而不是底层元素)的引用,因此您可以访问其方法。
7. Almost-components
假设您正在构建一个组件,以便您可以搜索人员。在您输入时,您会看到一个可能匹配的名称和照片列表。这样的东西。
(我正在寻找政治讽刺,因为我像大家一样,对其他人对政治的看法极为感兴趣。)
当设计此组件时,您可能会想到自己:该列表中的每个项目都是自己的SearchSuggestion组件?它真的只有几行HTML和CSS,也许不是?但我曾经被告知“如果有疑问,创造另一个组件”。
哦,我的,这是相当稀烂的一个泡菜,不是吗?
如果我是做这个,我不会有一个单独的组件。相反,只是一个renderSearchSuggestion方法返回每个条目的适当的DOM。然后结果就是下面的代码示例这样:
const SearchSuggestions = (props) => { // renderSearchSuggestion() behaves as a pseduo SearchSuggestion component // keep it self contained and it should be easy to extract later if needed const renderSearchSuggestion = listItem => ( <li key={listItem.id}>{listItem.name} {listItem.id}</li> ); return ( <ul> {props.listItems.map(renderSearchSuggestion)} </ul> ); }
如果事情变得更复杂,或者您想在其他地方使用此组件,则应该能够将代码复制/粘贴到新组件中。
不要过早组件化。组件不像茶匙;你可以有太多。(意思组件可以随便复制, 但是茶匙不行)
8. Components for formatting text
当我第一次开始使用React时,我想到组件应该是一个大东西,一种分组DOM的结构块的方法。但这样组件表现的很一般。
这里是一个<Price>组件,它接受一个数字,并返回一个漂亮的字符串,有或没有小数和一个'$'符号。
const Price = (props) => { const price = props.children.toLocaleString('en',{ style: props.showSymbol ? 'currency' : undefined,currency: props.showSymbol ? 'USD' : undefined,maximumFractionDigits: props.showDecimals ? 2 : 0,}); return <span className={props.className}>{price}</span> }; Price.propTypes = { className: React.PropTypes.string,children: React.PropTypes.number,showDecimals: React.PropTypes.bool,showSymbol: React.PropTypes.bool,}; Price.defaultProps = { children: 0,showDecimals: true,showSymbol: true,}; const Page = () => { const lambPrice = 1234.567; const jetPrice = 999999.99; const bootPrice = 34.567; return ( <div> <p>One lamb is <Price className="expensive">{lambPrice}</Price></p> <p>One jet is <Price showDecimals={false}>{jetPrice}</Price></p> <p>Those gumboots will set ya back <Price showDecimals={false} showSymbol={false}>{bootPrice}</Price> bucks.</p> </div> ); };
正如你可以看到,我使用强大的Intl字符串格式化库,这里有一个链接到他们的网站。
我应该指出(在一些朋克之前),这不是一行代码的保存。你可以很容易地使用函数来做到这一点。 (当然,组件只是具有不同形状括号的函数。)
这是更少的代码,但对我的眼睛,不太好:
function numberToPrice(num,options = {}) { const showSymbol = options.showSymbol !== false; const showDecimals = options.showDecimals !== false; return num.toLocaleString('en',{ style: showSymbol ? 'currency' : undefined,currency: showSymbol ? 'USD' : undefined,maximumFractionDigits: showDecimals ? 2 : 0,}); } const Page = () => { const lambPrice = 1234.567; const jetPrice = 999999.99; const bootPrice = 34.567; return ( <div> <p>One lamb is <span className="expensive">{numberToPrice(lambPrice)}</span></p> <p>One jet is {numberToPrice(jetPrice,{ showDecimals: false })}</p> <p>Those gumboots will set ya back {numberToPrice(bootPrice,{ showDecimals: false,showSymbol: false })} bucks.</p> </div> ); };
请注意,我不会检查我在上述任何一个有效的数字。那是因为 …
9. The store is the component’s servant
我可能写了这么几千次:
if (props.user.signInStatus === SIGN_IN_STATUSES.SIGNED_IN)..
(我被告知,我夸张,像,一个gazillion时代。)
最近我决定,如果我做这样的检查,我做错了。我想只问“是用户登录?”,而不是“用户的登录状态等于登录?”
的组件在他们的生命周期中所做的已经足够, 他们不应该去担心他们的父组件会传一些什么参数。 比如说Price不用管传入的数据是否是数字。
你会看到,如果你的store中的数据被设计为与您的组件匹配,您的组件将更加简单。我之前说过,复杂性是bug隐藏的地方。组件中的复杂性越低,bug出现的几率越低。
但是复杂这个问题肯定存在。
我的建议是:
制定你的组件的一般结构和他们需要的数据
设计您的store以支持这些要求
做任何你需要做的输入数据,使其适合store。
对于这最后一点,我建议一个单一的模块,所有的按传入的信息重命名props,将字符串转换为数字,将对象转换为数组,将日期字符串转换为日期对象。
如果你正在进行一个react/redux, 你可以在一个动作创建者中获取搜索结果:
fetch(`/api/search?${queryParams}`) .then(response => response.json()) .then(normalizeSearchResultsApiData) // the do-it-all data massager .then(normalData => { // dispatch normalData to the store here });
你的组件将会感谢你的。
10. Importing components without relative paths
不这样做的话后患无穷啊!
import Button from '../../../../Button/Button.jsx'; import Icon from '../../../../Icon/Icon.jsx'; import Footer from '../../Footer/Footer.jsx';
或者你可以这样做
import {Button,Icon,Footer} from 'Components';
理论上你可以:
但是当我写的代码我来认识到这是一个坏主意,有三个原因:当我写代码的时候, 我才认识到上面的模式并不好,原因有三个。
在Webpack2 似乎改变了原有的API。
eslint将会检测到错误, 由于找不到你引用的组件(因为resolve.alias)。
如果你使用一个好的IDE,它会知道你的组件在哪里。你会得到关于不提供所需props的提示, 也无法通过Command+click 打开文件这个功能。如果你这样做,你的IDE将不再知道在哪里找到该组件,你会失去这些给力的功能。
Wrap up
这就是全部, 我非常确定我将在今年看到这些模式的应用。 或许你们今天就会使用它。 你也可以分享一些你觉得不错的模式。
喔, 我决定我不关心你是否点击了绿色的心。
I WILL NOT BE DEFINED BY AN INTERNET METRIC.