The idea is to have value functions and rule functions which accept an object and let JSS render them dynamically. This will allow us __efficient__ dynamic theming. The idea has been around for a while, but this time me and @nathanmarks have got a plan.
{
button1: {
padding: 15,
// Value function.
color: props => props.buttonColor
},
// Rule function.
button2: props => ({
padding: props.padding,
color: props.color
})
}
We don't, we just call all dynamic values and compare new results with the current once.
We need to have a proof that calling all dynamic values on each update is an __actual__ perf problem. Right now this is a premature optimization, but if we need it, here are some options:
All suggestions are __NOT__ exclusive.
Caching is ensured by the user. It is an opt-in.
{
button: {
color: (nextProps, prevProps, prevValue) => {
// User can prevent rerender by comparing props manually.
// If user returns previous value, no rerendering happens.
if (nextProps.primary === prevProps.primary) return prevValue
return calculateColor(nextProps)
}
}
}
It is basically suggestion 1 + a helper function which allows nicer caching notation.
function cache(valueFn, ...propNames) {
return (nextProps, prevProps, prevValue) => {
if (hasChanged(propNames, nextProps, prevProps)) {
return valueFn(nextProps)
}
return prevProps
}
}
{
button: {
color: cache(props => calculateColor(props), 'primary')
}
}
Its a caching integrated into JSS itself.
{
button: {
// Once property "primary" has changed, JSS will call user's value function and get the new value.
color: [props => calculateColor(props), 'primary']
}
}
We need to make sure fallbacks are rendered first, like we already do (@mxstbr pointed this out).
Example
{
button: {
color: props => props.buttonColor
fallbacks: {
color: 'red'
}
}
}
We need an API which can be called with an object as param, so that JSS can call dynamic values/rules and take care of the rendering.
Example
const sheets = new SheetsManager()
sheets.update({primary: true})
We will try to rerender all dynamic values/rules. Caching mechanism will prevent performance issues.
So when sheets are updated rules will change but the class names remain the same right?
Yes, we need to keep class names static, because we have no way to update the class name on an element from jss stand point.
Updated the description.
Ok, I see that the end goal is to have an internal solution for JSS and not related to react-jss specific.
I just finished to pulish a fix for the PR that allows a ThemeProvider in React to inject a theme descriptor (meaning an object that defines variables) to provide those variables to all decorated Components using the current theme.
The usage is described in the README of my branch.
Definitely my approach will fulfill theming only for react users.
Just mentioning here that your approach for the ThemeProvider will break, because pure components (i.e. ones that return false from shouldComponentUpdate) block context updates. You need to have an event listener (like react-broadcast) to avoid this issue, which is what react-router, redux and also styled-components do. (see here)
@mxstbr I understand the fact that pure components won't update if context changes. But the components that consume the theme context are the wrappers (meaning the JSS Wrapper which won't be pure), not the wrapped components. I am not sure if I need to tweak the shouldComponentUpdate function, but I kinda feel confident that it won't affect wrapped components since the sheet prop injected by the JSS Wrapper is always different.
Anyway thanks for flagging that out, I would add some more unit tests to verify.
No, pure components _block_ context updates. If a component returns false from its shouldComponentUpdate, none of its children get the context update, even though they should.
Imagine a NeverRerender component:
class NeverRerender extends Component {
// Never rerender this component after the first render
shouldComponentUpdate() { return false; }
render() { return this.props.children; }
}
Now if you have a component tree like this:
<ThemeProvider theme={someTheme}>
<NeverRerender>
<ThemedButton />
</NeverRerender>
</ThemeProvider>
The ThemedButton will never ever update its theme, even if you change the theme prop of the ThemeProvider, because the NeverRerender component blocks context updates for its children.
This is why you need a react-broadcast-like solution and attach an event listener.
(See this very old issue for a huge discussion about this: https://github.com/facebook/react/issues/2517)
Please refocus on the proposed idea, which is a solution independent of react.
Gotcha now. Indeed a solution independent of react is ideal. Thanks again for the clarification
What about streams?
const sheet = jss.createStyleSheet()
const ruleStream = propsStream
// compose rule
.map(({buttonColor, padding, color}) => ({
button1: {
padding: 15,
color: buttonColor,
},
button2: {
color,
padding,
}
}))
// check if changed
.distinctUntilChanged()
const sheetStream = Observable.of(jss.createStyleSheet())
// combine sheet with rules
.combineLatest(ruleStream, (sheet, rules) => sheet.setRules(rules))
// attach on every change
.do((sheet) => sheet.attach())
.finally(() => sheet.detach())
Also something related https://github.com/davidkpiano/RxCSS
Streams seems to be out of scope for this issue, it can be a separate library. This issue is about a lower level implementation I think. I would be more than happy if someone does a streaming abstraction on top of jss though.
I am concerned about whether the keyword props might end up confusing for React users as in React props is a distinct concept that describes component inputs. Alternatively we could use vars as for Sheet Variables or Theme Variables. The latter term is commonly used for css based themes.
I think if you use it with react, you will really pass the props, similar to styled-components.
It wasn't clear for me on the proposal, so props can be updated on a per style sheet level. 馃憤
I think if you use it with react, you will really pass the props, similar to styled-components.
I thought about this more. Because the class names remain static as you said, this wouldn't work for React. Normally we share a StyleSheet among the same components, changing the StyleSheet according to one's individual props will change the appearance of all other components of the same kind.
Styled-Components creates new class names for each change, that can be reused among the same components.
We would need to have a similar approach. Maybe having a class name for the static part and a class name for the dynamic one, that varies.
@cvle
We definitely need to append new variations of rules. We shouldn't be replacing existing since as you rightly mentioned, it will change the look/feel for all instances.
@cvle looking into it again: yeah, dynamic rules can't share class names between elements.
Here is an update on the progress:
I have introduced a sheet.update(data) method which will call all function values with that data argument and render the returned result.
In "react-jss" I will split a sheet into 2 sheets: static and dynamic. I will walk over styles object and move props with function values into separate styles object.
const styles = {
button: {
float: 'left',
color: (data) => data.color
}
}
// will be compiled by react-jss (later by a separate package) into those 2:
// Static Sheet.
const styles = {
button: {
float: 'left'
}
}
// Dynamic Sheet.
const styles = {
button: {
composes: staticSheet.getRule('button'),
color: (data) => data.color
}
}
React-jss user will use the dynamic sheet object, the static one will stay an internal detail.
@injectSheet({
button: {
float: 'left',
color: (props) => props.color
}
})
class Button extends Component {
render() {
return <button className={this.props.classes.button}></button>
}
}
Will result in
<button className="button-static-123456 button-dynamic-123456"></button>
Right now plugins do not react on dynamic values changes, this will be part of a separate feature, probably a new hook onChangeValue, so that plugins like vendor-prefixer or default-unit can implement this hook and modify the value before it is applied to the dom.
Probably we should not retry styled-components mistakes.
Instead of this we can keep rendering architecture consistent with React.js (+ React Native) by introducing Virtual CSS to JSS. It also could include a kind of Reconciliation algorithms and ReactDOMServer.
It means on each render() in React Web component we can calculate CSS from JSS.
That is a part of the one of our React Native applications which we developed for our customer a few months ago:
// ...
render() {
// ...
const fontSize = props.fixedFontSize ? INSIGHTS_FONT_SIZE : scaledFontSize || basicFontSize
const insightTextStyle = [
styles.itemText,
{ fontSize },
isScaling && styles.itemTextScaling,
]
const containerStyles = [
styles.item,
props.style,
isScaling && styles.itemScaling,
]
return (
<TouchableOpacity
activeOpacity={0.75}
style={containerStyles}
onPress={this._onCardPress}
>
{isScaling && (
<View style={[styles.itemInnerScaling]}>
<AutoText
style={styles.itemText}
maxHeight={maxContentHeight}
initialFontSize={scaledFontSize || BASIC_FONT_SIZE}
onComplete={({ fontSize }) => this.handleScaleComplete({ fontSize })}
>
{finalContent}
</AutoText>
</View>
)}
// ...
As you noticed - React Native uses Inline Styles approach because it's a JS-to-Native-Controls bridge.
For React Web we can use just something like:
render() {
const myCardStyle = stylesheet.renderToDOM([
styles.myCard,
styles.cardWithToolbar,
])
return (
<MyCard className={myCardStyle} />
)
}
Or:
render() {
const myCardStyle = JSSForDOM.render(this, [
styles.myCard,
styles.cardWithToolbar,
])
return (
<MyCard className={myCardStyle} />
)
}
But there is only one problem - render() should not modify anything in a some reasons.
See also:
Merged.
released
Most helpful comment
Here is an update on the progress:
I have introduced a
sheet.update(data)method which will call all function values with that data argument and render the returned result.In "react-jss" I will split a sheet into 2 sheets: static and dynamic. I will walk over styles object and move props with function values into separate styles object.
React-jss user will use the dynamic
sheetobject, the static one will stay an internal detail.Will result in