Is your feature request related to a problem? Please describe.
We use the className property in a few select cases to apply css rules that we otherwise cannot apply through styles. (e.g. ::-webkit-scrollbar rules, etc). After updating to the latest version of react-native-web we now receive continuous warnings stating that the className property is deprecated without providing any documentation for an alternative to achieve the same result.
Describe a solution you'd like
Provide alternative instructions on how we can achieve the same result without using the className property.
Describe alternatives you've considered
We currently patch the react-native-web package after installing to comment out the warning line. While this suppresses the repeated errors it does not provide us a solution for upgrading in the future.
Additional context
react-native: 0.59.2
react-native-web: 0.11.0
There is not going to be a replacement API for adding className to primitives. You can still use setNativeProps or the unstable createElement export.
How would we use setNativeProps to combine our additional className with the one(s) generated by react-native-web. Is there a concrete example of this in the react-native-web documentation?
@RyanThomas73 Use setNativeProps inside component did mount.
Yes use it like any other instance method. You can read more in the "direct manipulation" docs.
@steida @necolas Yes but won't using it overwrite the className values set by react-native-web as a result of the style values?
The current use of className property combines class name with the className(s) generated by the style objects.
Yes but won't using it overwrite the className values set by react-native-web as a result of the style values?
Oh yes it will. Use a data-* attribute and target that with your CSS. Generally you should migrate away from using external CSS and bring up any use cases that might be worth accommodating
@necolas Thanks I'll try using the data-* attribute. I agree we should avoid using external css as much as possible but there are still plenty of cases like the ::-webkit-scrollbar css rules we're using which react-native-web doesn't support.
It's important for us to have a way to use the features that react-native-web either decides not to support or has not yet had time to implement.
People need to share relevant use cases (not solutions)
I want to add a hover on my react-native-paper menu, I'd like to hook a css and target the div, but I can't without getting warning, any solution (I just give you a specific use case)
@necolas We're doing server side rendering with RNW, but we also need media queries for our desktop application as it's important that the app looks correct on the first render or the markup coming from the server. We need to inject a className to our components to do this, but currently as we need a ref to the component and run setNativeProps on the server to accomplish this, it's impossible for us to get these classNames in our components on the server render and for them to look good on the web on first paint with the current APIs offered.
I'm mentioning this as an use case that we have.
Similar to the points brought up here: https://github.com/necolas/react-native-web/issues/1146#issuecomment-481737842
@necolas but to set a data-* attribute we'd also need to use setNativeProps right?
Forwarding of data-* props is no longer supported. Use dataSet instead. For example, dataSet={{ someName: 1 }}.
https://github.com/necolas/react-native-web/releases/tag/0.13.0
Similar to @TheMightyPenguin, I am trying to server-side render RNW using Next.js. If I use Dimensions to conditionally render styles, the server doesn't necessarily match the client's first render, which breaks the app (see https://github.com/vercel/next.js/discussions/14469).
I've come up with 2 solutions:
I've relied on styled-components (not styled-components/native) in order to generate these CSS media queries. If the className prop weren't deprecated, this solution would work well.
I know @necolas recommends against using styled-components like that, but it's the easiest way I've found to inject CSS media queries in my web files. styled-components also has good integrations for both React Native and Next.js, making this a really nice DX.
The unfortunate part, of course, is that styled-components relies on className exclusively, and doesn't allow you to use a data-*/dataSet.X prop as a CSS selector.
Is this considered a strong enough use case for using the className @necolas? I'm not sure how else to generate CSS media query styles from JS.
If not β is anyone aware of a package that generates CSS modules based on custom selectors, which could also work for server-side rendering/Next.js and RNW?
I'm happy to share minimal code to illustrate if that would help.
@nandorojo I actually tried to make it work with styled-components, and used the className in a data-class attribute, but that just overcomplicated things as now we needed to modify the CSS we generated on the server to include this selector. This was too complicated, and also it's not the direction React Native Web is moving so in the long run this was going bite us, so we decided to embrace handling responsive styles in JavaScript like RNW recommends.
We ended up doing an approach similar to this one, and when rendering on the server, we use the user-agent header to know what styles to render initially. It's not as precise as using a CSS media querie based on the resolution, but it's working well for us, and it definitely goes more aligned with the principles of RNW and it's easier to implement. Hope this helps!
@TheMightyPenguin I see, thanks for the heads up. Maybe I could rely on the same solution.
The downside is that for some percentage of users, the user agent will be wrong, so it doesn't feel like a safe workaround. If I rely on screen size on from the server, someone could resize their screen between the time that they first opened the URL and the time the client renders. If they resize to a different breakpoint during that time, the client & server props won't match, and the correct styles won't apply since the Next.js app will break.
I would definitely prefer to keep this all in JS if I could. It makes the most sense, and I agree that it's the right direction for RNW. Unfortunately, the trade off of losing good SEO seems too high. And a solution that doesn't work for every user makes me a bit nervous to put in production.
@nandorojo is that actually an use case where your users are resizing the page much? In our case it's not that common, I think it's an edge case you shouldn't worry too much about π . If users resize on the client and the page has already rendered, it's fine as styles will be handled in JavaScript, so styles will change nicely. Also on the server you cannot rely on a screen size as you don't have that information, but instead on the user agent string which will look something like this:
Mozilla/5.0 (iPhone; CPU iPhone OS 12_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.0 Mobile/15E148 Safari/604.1
https://deviceatlas.com/blog/list-of-user-agent-strings
So you use that to determine what kind of device the user is using (mobile, tablet, desktop, ...), and then apply styles based on that, and there are also many libraries that parse this string and help you to detect the device.
Regarding SEO, why is that a concern? Rendering the mobile/tablet/desktop version of your markup shouldn't affect SEO if all follow semantic HTML practices, or if you have things like JSON-LD structured data, so I'm not sure you would lose your SEO score.
@TheMightyPenguin Yeah, I don't think it's a super common situation. It's just an edge case I would like to prevent. I'll keep testing it though. I think you can get the screen width using client-hints actually, but I'll try both that and the user-agent.
The reason I would sacrifice SEO would be if I lazy-load the components and stick with JS styles. React SSR requires that all components from the first client render & the server render have the same props. If they aren't, then it will render whatever the server generated and it won't re-hydrate (see: https://github.com/vercel/next.js/discussions/14469). I'll see if I can make it work with user-agent for now, and hopefully I can just avoid edge cases.
@nandorojo Nice! I didn't know about client-hints, thanks!
Also, you can just render SSR and if props don't match react will just do a full render right? In that case the worst case scenario is that for some users they will have an extra initial render on the client, but I guess you'll have to see how many users does that affect
Also, you can just render SSR and if props don't match react will just do a full render right?
That's what I thought:
I guess my question is β why does the style prop have to match? Props change in react all the time. Canβt the app just rehydrate the changes when it opens on the client?
Turns out, React requires your server and client to be the same, and if they aren't it will still render whatever the server generated.
What you're attempting to do requires a DOM (window or document to be present) and the server doesn't have one. What you're asking for is to essentially disregard/throw away the SSR generated content (perhaps generating markup and using data for a mobile device) and possibly destroying/recreating it on the client (perhaps regenerating markup and refetching data for a wide-screen desktop monitor). This translates to (possibly) rendering two different apps for each initial application load.
There's more info about that on Google's website: https://developers.google.com/web/updates/2019/02/rendering-on-the-web
In any case, there appears to be a trade-off with deprecating className if you want 1) responsive styles, and 2) server side rendering. If you just use CSS, there is no such trade-off (other than the fact that we want to stay away from CSS.)
If you have to do a full re-render, you're getting "one app for the price of two" performance-wise, as Google puts it.
I guess we can bank on phones and internet getting faster, so maybe this won't be a problem going forward. The downside is that I don't know how to check from the client if the props are different from the server. So if I want to avoid the edge case of client and server not matching for the edge case I described above, it might require a re-render no matter what.
I appreciate your help with this, by the way.
Yeah, I guess the "double render" is a price we're going to pay in some requests, but I doubt it will be that often, we'll be monitoring our logs to see often we get a props mismatch due to this to see if it's a problem worth solving π For a bit more context, what we do is something like:
// where our app renders in SSR
<DeviceProvider initialDevice={initialDeviceFromUserAgent}>
<App />
</DeviceProvider>
// some component
const SomeComponent = () => {
const device = useGetDeviceType();
if (device === 'mobile') {
return <MobileJsx />
}
return <DesktopJsx />
};
At the begining I was skeptic about using a device name instead of a breakpoint, but it's actually quite nice!
And likewise π It's nice talking with others with the same challenges π !
I see, thanks for sharing.
One thing I did notice just now - while that might work for server side rendering, it doesn't work for Next.js's Static Site Generation. I should have been more clear in my wording earlier for what I'm using.
Static Site Generation can't rely on the user agent since the pages are generated at build time. And so, a forced re-render would be necessary basically every time.
Also, doesn't user agent only tell you the device but not size? What if a desktop browser isn't full screen width?
Here is my current solution.
The downside is that there is a flash of unstyled components π€
// work-around to use useLayoutEffect on SSR
// this will only run on the client
const useIsomorphicLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect
function useScreenWidth() {
const [width, setWidth] = useState(
// on web, we set this to 0
// this ensures that our initial render on SSR and the client always has the same props
// which silences the error
Platform.select({ web: 0, default: Dimensions.get('window').width })
)
useIsomorphicLayoutEffect(() => {
if (Platform.OS === 'web') {
// update the width in useLayoutEffect
const { width } = Dimensions.get('window')
if (width >= breakpoints[0]) {
// we'd get the breakpoints variable somewhere
setWidth(width)
}
}
}, [])
useEffect(() => {
const listener = (event) => {
setWidth(event.window.width)
}
Dimensions.addEventListener('change', listener)
return () => {
Dimensions.removeEventListener('change', listener)
}
}, [])
return width
}
I don't know the performance implications of useLayoutEffect running on mount, but it seems to be working for Next.js.
In an effort to not spam people who are subscribed to this issue, I'll be moving discussion to https://github.com/nandorojo/dripsy/issues/1. Please follow up there if you are having issues or have solved this problem in another way.
@nandorojo right, with static site generation the only way would be with CSS media queries π’
Also, doesn't user agent only tell you the device but not size? What if a desktop browser isn't full screen width?
Yeah, you're right, it's not a perfect solution but is a place to start!
@steida thanks so much for sharing. Looks like I'll be using fresnel.
Most helpful comment
Oh yes it will. Use a
data-*attribute and target that with your CSS. Generally you should migrate away from using external CSS and bring up any use cases that might be worth accommodating