Контекстная типография в React Native
Хотя я уже давно использую React Native, я никогда не был доволен подходами, которые я видел, когда речь шла о компонентах Text. Я хотел решение, которое было бы больше похоже на CSS. Хотя React Native пытается эмулировать стили, как в Интернете, через библиотеку под названием Yoga, он не обеспечивает каскадную функциональность.
Все стили в React Native эффективно встроены и поэтому имеют схожие проблемы, с которыми сталкивается веб-экосистема с момента появления CSS in JS:
- Медиа-запросы
- Глобальные стили (шрифты, стили по умолчанию)
- Псевдо стили
Проблема
Мне нужна возможность применять базовый стиль ко всем текстовым компонентам в приложении. Это включает в себя такие стили, как:
- Font family
- Font size
- Стиль шрифта
- Цвет
Я также хотел бы переопределить стили, например части приложения, которые могли бы хотеть другой размер шрифта или быть курсивом.
Решение первое: модуль стиля
Простой подход состоит в том, чтобы объявить базовый стиль как модуль и импортировать его при использовании компонентов Text:
import React from "react"; import { View, Text } from "react-native"; import textStyles from "./textStyles"; const styles = { container: { flex: 1, justifyContent: "center", alignItems: "center" }, title: { fontSize: 40, lineHeight: 40 * 1.4, fontWeight: "500" } }; const App = () => (); export default App; Welcome Hello World
const textStyles = { fontFamily: "Helvetica", fontWeight: "normal", fontSize: 16, lineHeight: 22.4, color: "black" }; export default textStyles;
Несмотря на простоту, у этого подхода есть пара проблем:
- Представления становятся заваленными style props.
- Легко подвержен случаям, когда стиль пропускается.
Решение второе: пользовательский компонент
Как подробно описано в официальной документации, другой подход заключается в инкапсуляции базовых стилей в пользовательский компонент Text с возможностью переопределения определенных стилей:
import React from "react"; import { View } from "react-native"; import { MyAppText, textStyles } from "./MyAppText"; const styles = { container: { flex: 1, justifyContent: "center", alignItems: "center" }, heading: (scale) => { const fontSize = textStyles.fontSize * scale; return { lineHeight: fontSize * 1.4, marginBottom: 12, fontWeight: "500", fontSize }; }, paragraph: { marginBottom: 12 } }; const App = () => (); export default App; Heading 1 Heading 2 Heading 3 Heading 4 Heading 5 Heading 6 {"\n"} Paragraph Italics Bold Underline
import React from "react"; import { Text } from "react-native"; export const textStyles = { fontFamily: "Helvetica", fontWeight: "normal", fontSize: 16, lineHeight: 22.4, color: "black" }; export const MyAppText = ({style, ...props}) => ();
В документации также объясняется, что компоненты Text имеют концепцию наследования стилей. Однако вложение MyAppText приводит к случаям, когда некоторые стили игнорируются из-за перезаписи базового стиля:
// Вложение с MyAppText перезаписывает fontStyle с textStyle.fontStyle.// Вложенность с текстом позволяет наследовать стиль работать должным образом Hello World Hello World
В целом, у этого подхода есть пара проблем:
- Описанная выше проблема компоновки делает запутанным требование, чтобы части приложения использовали компоненты Text и MyAppText для достижения желаемого эффекта.
- Наследование стилей работает только с текстовыми деревьями компонентов.
Решение третье: контекстный API
Примечание. Чтобы эта статья была достаточно короткой, я рекомендую прочитать документацию React, если вы еще не знакомы с новым Context API.
Параметр defaultValue, используемый для создания компонентов Context Consumer и Provider, представляет собой значение, полученное компонентами Consumer, если в дереве компонентов не найден поставщик-предок. Установка этого параметра в базовый стиль даст эффект, аналогичный предыдущим решениям.
Чтобы получить каскадный эффект, компонент Provider будет использоваться для изменения значения, получаемого компонентами Consumer. Его значение будет результатом слияния значения Consumer со стилем prop.
import React from "react"; import { ScrollView, SafeAreaView } from "react-native"; import { MyAppText, textStyles } from "./MyAppText"; import appStyles from "./appStyles"; const styles = { heading: (scale) => { const fontSize = textStyles.fontSize * scale; return { lineHeight: fontSize * 1.4, marginBottom: 12, fontWeight: "500", fontSize }; } } const App = () => (); export default App; {/* All MyAppText components are contextually set to the base styling. */} Heading 1 Heading 2 Heading 3 Heading 4 Heading 5 Heading 6 {/* All MyAppText components will now be contextually set to #333. */} Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. {/* Hack to mimic paragraph spacing. Setting the color has triggered the text layout and therefore marginBottom doesn't work as expected. However, without applying filtering logic for non-cascading styles, every MyAppText would have a marginBottom style which wouldn't be ideal. */} {"\n\n"} {/* All MyAppText components will now be contextually set to bold #333. */} Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?
import { Platform, StatusBar } from "react-native"; const appStyles = { safeAreaContainer: { flex: 1, marginTop: Platform.OS === "android" ? StatusBar.currentHeight : 0 }, container: { flex: 1, padding: 8 }, paragraph: { marginBottom: 12 } }; export default appStyles;
import React from "react"; import { Text } from "react-native"; export const textStyles = { fontFamily: "Helvetica", fontStyle: "normal", fontSize: 16, lineHeight: 22.4, color: "black" }; const { Provider, Consumer } = React.createContext(textStyles); export const MyAppText = ({style, ...props}) => ({(contextStyle) => { if (style) { const mergedStyle = {...contextStyle, ...style}; return ( );); } else { return ( ); } }}
К сожалению, у этого подхода есть пара проблем:
- Как и в предыдущем решении, контекстное моделирование ограничено только текстовыми деревьями компонентов.
- Некоторые стили не должны каскадироваться, такие как padding и margin, и вместо этого непосредственно применяться к компоненту Text.
Решение второй проблемы означало бы, что MyAppText имитирует функциональность, предоставляемую Text «из коробки». Однако теперь больше нет необходимости использовать текст при вложении, как в предыдущем решении:
// Отображается курсивом, как и ожидалось.Hello World
Решение четвертое: контекстный API (версия 2)
Этот подход основан на предыдущем решении, но вместо этого выделяет две основные проблемы MyAppText в отдельные компоненты:
- StyleText - изменяет значение стиля контекста.
- MyAppText - использует контекстное значение стиля и style prop.
import React from "react"; import { View, ScrollView, SafeAreaView } from "react-native"; import { StyleText, MyAppText, textStyles } from "./MyAppText"; import appStyles from "./appStyles"; const styles = { heading: (scale) => { const fontSize = textStyles.fontSize * scale; return { lineHeight: fontSize * 1.4, marginBottom: 12, fontWeight: "500", fontSize }; } } const App = () => (); export default App; {/* All MyAppText components are contextually set to the base styling. */} Heading 1 Heading 2 Heading 3 Heading 4 Heading 5 Heading 6 {/* All MyAppText components will now be contextually set to #333. */} {/* Note: View components are allowed within the tree. */} Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. {/* All MyAppText components will now be contextually set to bold #333. */} {/* Note: StyleText is nested to compose styles throughout the tree. */} {/* Note: View components are allowed within the tree. */} Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?
import { Platform, StatusBar } from "react-native"; const appStyles = { safeAreaContainer: { flex: 1, marginTop: Platform.OS === "android" ? StatusBar.currentHeight : 0 }, container: { flex: 1, padding: 8 }, paragraph: { marginBottom: 12 } }; export default appStyles;
import React from "react"; import { Text } from "react-native"; export const textStyles = { fontFamily: "Helvetica", fontStyle: "normal", fontSize: 16, lineHeight: 22.4, color: "black" }; const { Provider, Consumer } = React.createContext(textStyles); export const MyAppText = ({style, ...props}) => ({(contextStyle) => { const mergedStyle = style ? {...contextStyle, ...style} : contextStyle; return ( ); export const StyleText = ({style, children}) => (); }} {(contextStyle) => ( );{children} )}
MyAppText значительно упрощен и больше не требует логики для определения того, какие стили должны / не должны каскадироваться. Это связано с тем, что каскадные стили должны передаваться в StyleText, а специфичные для компонента стили должны передаваться непосредственно в MyAppText.
Кроме того, поскольку StyleText ничего не отображает, он больше не ограничен только текстовыми деревьями компонентов. Стили могут быть изменены для целых поддеревьев, и компоненты-потомки MyAppText получат измененный контекстный стиль.