以下模式在我的React应用程序代码库中重复了一遍:
const {items, loading} = this.props
const elem = loading
? <Spinner />
: items.length
? <ListComponent />
: <NoResults />
虽然这比嵌套实际的 if/else
子句更清晰,但我正试图拥抱更优雅和功能性的模式。我已经读过使用像Either
monad这样的东西,但是我所做的所有努力最终看起来都更加冗长,而且可重用性更低(考虑到我正在尝试,这个伪代码可能不起作用记住以前的尝试):
import {either, F, isEmpty, prop} from 'ramda'
const isLoading = prop('loading')
const renderLoading = (props) => isLoading(props) ? <Spinner /> : false
const loadingOrOther = either(renderLoading, F)
const renderItems = (props) => isEmpty(props.items) ? <NoResults /> : <ListComponent />
const renderElem = either(loadingOrOther, renderItems)
const elems = renderElem(props)
我可以使用哪种模式更干/可重复使用?
谢谢!
答案 0 :(得分:5)
虽然这比嵌套实际
if/else
条款更清晰render () { const {items, loading} = this.props return loading ? <Spinner /> : items.length ? <ListComponent items={items} /> : <NoResults /> }
你发布了不完整的代码,所以我填补了一些空白,以获得更具体的例子。
查看您的代码,我发现很难读取条件的位置以及返回值的位置。条件分散在各种缩进级别的各行中 - 同样,返回值也没有视觉一致性。事实上,loading
中的return loading
甚至不是一个条件,直到您进一步阅读该程序才能看到?
。在这种情况下选择要呈现的组件是 flat 决策,并且代码的结构应该反映出来。
使用if/else
会在此处生成一个非常可读的示例。没有嵌套,您可以看到返回的各种类型的组件,整齐地放在相应的return
语句旁边。这是一个简单的平面决策,只需要一个简单的详尽的案例分析。
我在这里强调详尽这个词,因为您决定至少提供if
和else
选择分支是很重要的。在您的情况下,我们有第三个选项,因此使用了一个else if
。
render () {
const {items, loading} = this.props
if (loading)
return <Spinner />
else if (items.length)
return <ListComponent items={items} />
else
return <NoResults />
}
如果您查看此代码并尝试“修复”它,因为您认为“拥抱更优雅和功能性模式”,您就会误解“优雅”和“功能性”。
嵌套三元表达式没有任何优雅。功能编程不是用最少的键击来编写程序,导致程序过于简洁且难以阅读。
像我使用的那样的 if/else
语句在某种程度上不那么“功能”,因为它们涉及不同的语法。当然,它们比三元表达式更冗长,但它们的运行方式正如我们所希望的那样,它们仍然允许我们声明功能行为 - 不要让语法单独强迫你做出愚蠢的决定编码风格。
我同意遗憾的是,if
是JavaScript中的语句,而不是表达式,但这正是您所使用的。你仍然能够制作出具有这种约束的优雅和功能性程序。
<强>说明强>
我个人认为依赖于真实的价值观。我宁愿把你的代码写成
render () {
const {items, loading} = this.props
if (loading) // most important check
return <Spinner />
else if (items.length === 0) // check of next importance
return <NoResults />
else // otherwise, everything is OK to render normally
return <ListComponent items={items} />
}
与代码相比,这不太可能吞下错误。例如,假装你的组件以某种方式具有loading={false} items={null}
的prop值 - 你可以说你的代码会优雅地显示NoResults
组件;我认为你的组件处于非加载状态且没有项目是错误的,我的代码会产生错误以反映:Cannot read property 'length' of null
。
这告诉我在这个组件的范围之上发生了一个更大的问题 - 即这个组件有loading = true 或某些项目数组(空或其他);没有其他道具组合是可以接受的。
答案 1 :(得分:3)
我认为你的问题不是关于if语句与三元组的关系。我认为您可能正在寻找不同的数据结构,允许您以强大的DRY方式抽象条件。
有一些数据类型可以派上用场抽象。例如,您可以使用Any
或All
monoid来抽象相关条件。您可以使用Either
或Maybe
。
您还可以查看Ramda的cond
,when
和ifElse
等功能。你已经看过总和类型了。这些都是特定背景下强大而有用的策略。
但根据我的经验,这些策略确实超出了观点。在视图中,我们实际上希望可视化层次结构,以便了解它们将如何呈现。所以三元组是一个很好的方法。
人们可以不同意“功能”的含义。有人说功能编程是关于纯度或参考透明度;其他人可能会说它只是“用功能编程”。不同的社区有不同的解释。
因为FP对不同的人意味着不同的东西,所以我将专注于一个特定的属性,即声明性代码。
声明性代码在一个地方定义算法或值,并且不会强制改变或变异。声明性代码声明是的内容,而不是通过不同的代码路径强制将值分配给名称。您的代码目前是声明性的,这很好!声明性代码提供保证:例如“这个函数肯定会返回,因为return
语句在第一行”。
这个错误的概念是三元嵌套,而if语句是扁平的。这只是格式化问题。
return (
condition1
? result1
: condition2
? result2
: condition3
? result3
: otherwise
)
将条件放在自己的行上,然后嵌套响应。您可以根据需要多次重复此操作。最后的“else”就像任何其他结果一样缩进,但它没有条件。它可以根据您的需要扩展到尽可能多的情况。我已经看到并用这样的许多扁平三元组写了视图,我发现更容易完全遵循代码,因为路径没有分开。
你可以认为if
语句更具可读性,但我认为再次阅读对不同的人来说意味着不同的东西。所以要解开那个,让我们考虑一下我们强调什么。
当我们使用三元时,我们强调只有一种可能的方式来声明或返回某些东西。如果函数只包含表达式,那么我们的代码更有可能作为公式读取,而不是公式的实现。
当我们使用if语句时,我们强调单独的,分开的步骤来产生输出。如果您更愿意将您的观点视为单独的步骤,那么如果陈述有意义。如果您希望将视图视为基于上下文的具有不同表示的单个实体,则三元组和声明性代码会更好。
总而言之,您的代码已经正常运行。可读性和易读性是主观的,专注于您想要强调的内容。不要觉得表达式中的多个条件是代码气味,它只是代表UI的复杂性,解决这个问题的唯一方法(如果需要解决)就是改变UI的设计。 UI代码是复杂的,让代码诚实,代表所有潜在的状态并不是一件容易的事。
答案 2 :(得分:2)
您可以使用和类型和模式匹配来避免if/else
语句。由于Javascript不包含这些功能,您必须自己实现它们:
const match = (...patterns) => (...cons) => o => {
const aux = (r, i) => r !== null ? cons[i](r)
: i + 1 in patterns ? aux(patterns[i + 1](o), i + 1)
: null;
return aux(patterns[0](o), 0);
};
match
需要一堆模式函数,构造函数和数据。除非匹配,否则针对数据测试每个模式函数。然后使用成功模式函数的结果调用相应的构造函数,并返回最终结果。
为了使match
能够识别模式匹配是否不成功,模式必须实现一个简单的协议:每当模式不匹配时,该函数必须返回null
。如果模式匹配但相应的构造函数是一个无效的构造函数,则它必须只返回一个空的Object
。这是微调框的模式函数:
({loading}) => loading ? {} : null
由于我们使用解构赋值来模仿模式匹配,我们必须将每个模式函数包装在try/catch
块中,以避免在解构期间出现未捕获的错误。因此,我们不是直接调用模式函数,而是使用特殊的应用程序:
const tryPattern = f => x => {
try {
return f(x);
} catch (_) {
return null;
}
};
最后,这是一个微调框的构造函数。它不需要参数并返回一个JSX微调元素:
const Spinner = () => <Spinner />;
让我们把它们放在一起看看它是如何工作的:
// main function
const match = (...patterns) => (...cons) => x => {
const aux = (r, i) => r !== null ? cons[i](r)
: i + 1 in patterns ? aux(patterns[i + 1](x), i + 1)
: null;
return aux(patterns[0](x), 0);
};
// applicator to avoid uncaught errors during destructuring
const tryPattern = f => x => {
try {
return f(x);
} catch (_) {
return null;
}
};
// constructors
const Spinner = () => "<Spinner />";
const NoResult = () => "<NoResult />";
const ListComponent = items => "<ListComponent items={items} />";
// sum type
const List = match(
tryPattern(({loading}) => loading ? {} : null),
tryPattern(({items: {length}}) => length === 0 ? {} : null),
tryPattern(({items}) => items !== undefined ? items : null)
);
// mock data
props1 = {loading: true, items: []};
props2 = {loading: false, items: []};
props3 = {loading: false, items: ["<Item />", "<Item />", "<Item />"]};
// run...
console.log(
List(Spinner, NoResult, ListComponent) (props1) // <Spinner />
);
console.log(
List(Spinner, NoResult, ListComponent) (props2) // <NoResult />
);
console.log(
List(Spinner, NoResult, ListComponent) (props3) // <ListComponent />
);
现在我们有一个List
和类型,其中包含三个可能的构造函数:Spinner
,NoResult
和ListComponent
。输入(props
)确定最终使用的构造函数。
如果List(Spinner, NoResult, ListComponent)
对您来说仍然过于费力,并且您不想明确列出List
的各个状态,则可以在求和类型定义期间传递构造函数:
const List = match(
tryPattern(({loading}) => loading ? {} : null),
tryPattern(({items: {length}}) => length === 0 ? {} : null),
tryPattern(({items}) => items)
) (
Spinner,
NoResult,
ListComponent
);
现在,您只需以非常干的方式致电List(props1)
等。
match
将以静默方式返回null。如果您希望保证至少有一个模式成功匹配,您也可以抛出错误。
答案 3 :(得分:1)
由于Ramda具有ifElse
函数,您可以使用它以可重复使用的无点样式编写条件。
Runnable示例(使用字符串而不是<Tags>
,因此它可以作为堆栈代码运行。)
const { compose, ifElse, always, prop, isEmpty } = R;
const renderItems = ifElse(isEmpty, always('noResults'), always('listComponent'));
const renderProps = ifElse(
prop('loading'),
always('spinner'),
compose(renderItems, prop('items'))
);
// usage: const elem = renderProps(this.props);
// test
console.log(renderProps({ loading: true, items: ['a', 'b', 'c'] }));
console.log(renderProps({ loading: false, items: [] }));
console.log(renderProps({ loading: false, items: ['a', 'b', 'c'] }));
<script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.25.0/ramda.min.js"></script>
当然,另一种选择是使用箭头函数和条件运算符将条件分成两个函数。与上面的示例一样,这为您提供了可重用的renderItems
函数:
const renderItems = list => list.length ? 'listComponent' : 'noResults';
const renderProps = props => props.loading ? 'spinner' : renderItems(props.items);
// usage: const elem = renderProps(this.props);
// test
console.log(renderProps({ loading: true, items: ['a', 'b', 'c'] }));
console.log(renderProps({ loading: false, items: [] }));
console.log(renderProps({ loading: false, items: ['a', 'b', 'c'] }));
答案 4 :(得分:0)
您无需为此安装额外的软件包:
content() {
const {items, loading} = this.props
if (loading) {
return <Spinner />;
}
return items.length ? <ListComponent /> : <NoResult />;
}
render() {
return this.content();
}