jaredh159 / tailwind-react-native-classnames

simple, expressive API for tailwindcss + react-native
2.01k stars 82 forks source link

setColorScheme don't change the theme #97

Closed Bryant-Anjos closed 2 years ago

Bryant-Anjos commented 2 years ago

Hi! I'm trying to change the app theme when the user changes it inside the app. But it's not working. The code example is below. Is there something wrong?

useEffect(() => {
  tw.setColorScheme(darkTheme ? 'dark' : 'light')
}, [darkTheme])

<View style={tw`bg-white dark:bg-gray-900`}>
jaredh159 commented 2 years ago

where is the darkTheme variable coming from? is it controlled by useState? Or some other mechanism?

One thing to be careful of is that React.useMemo() can prevent re-renders when the colorScheme changes high up in the component hierarchy. Are you using useMemo() or React.PureComponent anywhere?

pix2D commented 2 years ago

I have found the problem to be that if you're not checking for dark mode within the screen/component it is not going to re-render after dark mode changes. If you reload the app with dark mode active the styles work.

Try adding this to a component/screen you're testing and see if it changes then...

import { useColorScheme } from 'react-native'

// ...

const isDarkMode = useColorScheme()

@jaredh159 Is every component supposed to re-render after dark mode changes? If not; do we need to manually pull in useColorScheme everywhere to make it work? I'm just using darkMode: 'media' in tailwind.config.js btw, not changing via setColorScheme; and I've done this also.

jaredh159 commented 2 years ago

Every component does not need to pull in useColorScheme. Most users shouldn't ever need to even pull in that function from react native, instead you should be able to use the useDeviceContext once at the root of your component heirarchy, and when dark mode changes, all of the components should re-render, provided they're not memoized with useMemo or React.PureComponent, or have some sort of custom componentShouldUpdate logic. Be sure to check the readme for how to configure with useDeviceContext().

Now, if you're using RN for web, I can't really speak much to that. This library does not aim to support RN web, although I know people are using it in that context.

pix2D commented 2 years ago

Ok that's what I thought. It does work directly in my App.tsx however for any code outside it does not (I have useDeviceContext setup and nothing is memoized or using PureComponent). Is there anything extra required for React Navigation? I'm not sure how to debug further from this. Here's my super basic App.tsx:

import { NavigationContainer } from '@react-navigation/native'
import tw from 'lib/tailwind'
import AppTabNavigator from 'navigators/AppTab'
import { SafeAreaProvider } from 'react-native-safe-area-context'
import { useDeviceContext } from 'twrnc'

export default function App() {
  useDeviceContext(tw)

  return (
    <SafeAreaProvider>
      <NavigationContainer>
        <AppTabNavigator />
      </NavigationContainer>
    </SafeAreaProvider>
  )
}

And my lib/tailwind.ts:

import { create } from 'twrnc'

const tw = create(require('../../tailwind.config.js'))

export default tw
Bryant-Anjos commented 2 years ago

where is the darkTheme variable coming from? is it controlled by useState? Or some other mechanism?

I'm using redux.

One thing to be careful of is that React.useMemo() can prevent re-renders when the colorScheme changes high up in the component hierarchy. Are you using useMemo() or React.PureComponent anywhere?

I don't. None of These.

In my project I'm using the react native paper, and I noticed when using the useTheme() in the same component the theme changes correctly.

jaredh159 commented 2 years ago

I'm a bit stumped then. I think I'll try to do some digging within the next day or two and see if I can replicate.

jaredh159 commented 2 years ago

@Bryant-Anjos I did a little digging today, and I'm having trouble replicating. Any chance your project is open source, where I could clone it and troubleshoot? Right now my best guess is that one of your dependencies is usine React.memo or useMemo somewhere in your component hierarchy, which is preventing the re-renders when the dark-mode changes.

rimsila commented 2 years ago

@Bryant-Anjos I did a little digging today, and I'm having trouble replicating. Any chance your project is open source, where I could clone it and troubleshoot? Right now my best guess is that one of your dependencies is usine React.memo or useMemo somewhere in your component hierarchy, which is preventing the re-renders when the dark-mode changes.

@jaredh159 In my case I'm not using memo or useMemo but it seem not working the first button click and second will work. here is my repo

Bryant-Anjos commented 2 years ago

@Bryant-Anjos I did a little digging today, and I'm having trouble replicating. Any chance your project is open source, where I could clone it and troubleshoot? Right now my best guess is that one of your dependencies is usine React.memo or useMemo somewhere in your component hierarchy, which is preventing the re-renders when the dark-mode changes.

@jaredh159 it's a private project, I cannot open source it. But I'll try reproduce the problem in an open source app later.

range-of-motion commented 2 years ago

I think I'm having the same problem.

I'm trying to use the dark: prefix, and it works, but I have to close the component and re-open it for a visual change. The component doesn't refresh by itself (if it's already open).

I'm using tw.setColorScheme('dark'); (hard-coding it to 'dark' just for debugging purposes).

If you any more information, let me know. I'm happy to provide it.

jaredh159 commented 2 years ago

If someone would be up for creating a very minimal reproduction open source repository, I'd be grateful, and I'd definitely dig in and try to find the root of the problem. Ideally just a plain expo RN app, with no other dependencies other than twrnc. I have an app like that locally that I test with, and I can't get the problem to manifest.

I appreciate that @rimsila linked to an open source project, but it's got lots of dependencies, and I wouldn't consider it a minimal reproduction.

That said, it's very possible there is some core bug or logic/render edge case here, since several of you are experiencing this. None of my RN apps actually use dark mode, so maybe that's why I haven't run into it. But, my apps do use the faux media-queries, which work pretty much exactly the same -- deeply nested components need to rerender when some device attribute changes (like switching from portrait to landscape) -- and my experience has been that it works great, as long as I don't have a memoized component in the tree preventing re-renders down the line. That's why I keep wondering if these problems might be due to third-party libraries implementing memoization somewhere.

If it turns out that memoization is in fact the culprit, it might be the case that enough common third party deps use that technique, that maybe this library needs to have some sort of alternate method of ensuring re-renders. I'm willing to consider that and work on something to that end, but first I need to figure out exactly what's going on, and could definitely use some help with that step.

range-of-motion commented 2 years ago

I'll try and set up something from scratch tonight.

range-of-motion commented 2 years ago

@jaredh159 I set up a barebones React Native project and installed this library. Check it out over here.

I have the feeling that either I'm massively overlooking something, because tw.setColorScheme() does nothing for me. However when I manually toggle iOS' dark mode (in "settings", then "developer", then "dark appearance") it works like a charm.

I would appreciate it if you could have a look and tell me if I'm doing something wrong, cheers 🙌

jaredh159 commented 2 years ago

@range-of-motion thanks for the reproduction. I see the problem now. When you manually use tw.setColorScheme() you're informing tw that the color scheme changed, but it doesn't automatically cause react to re-render. You'd need to (currently) wrap up that function in something that also sets state. This is a bit ugly, but works:

const [mode, setMode] = useState('light');

// [...] then, in your component somewhere...
<TouchableOpacity
  onPress={() => {
    tw.setColorScheme('dark');
    setMode('dark'); // 👋  <-- this line will trigger the re-render
  }
/>

Admittedly, this feels like a bit of a kludge. I think maybe what I should do is create another custom hook, something like useManualColorScheme() that bakes in the state, forcing the re-render. That would be a good bit more ergonomic.

jaredh159 commented 2 years ago

ok, 2.2.0 now has a useAppColorScheme() hook to make this easier. See docs for how to integrate:

https://github.com/jaredh159/tailwind-react-native-classnames#taking-control-of-dark-mode

Thanks for bringing this to my attention!

fristyr commented 2 years ago

@jaredh159 Hi, didn't want to open a new issue because it's a kinda suggestion then issue. So in the documentation, you added next

  const [colorScheme, toggleColorScheme, setColorScheme] = useAppColorScheme(tw);

Let's I need just setColorScheme And I cant do next

  const [ setColorScheme] = useAppColorScheme(tw);

And need to do next

  const [ ,  ,setColorScheme] = useAppColorScheme(tw);

Is it possible in the feature to do next ?

  const {setColorScheme, ...etc} = useAppColorScheme(tw);

Or I'm missing something? Thank you!

jaredh159 commented 2 years ago

@fristyr you would need to do [, , setColorScheme] = ... as you pointed out. I don't think I'll change it to an object, because it's more consistent with React's useState() api, and I don't think a couple commas is too big of a deal.

fristyr commented 2 years ago

@jaredh159 No no, it's not a big deal for sure. Good that I did not open a new issue for this simple question. Anyways thanks! Working properly

jaredh159 commented 2 years ago

@fristyr sure thing, thanks for the feedback. if it turns out that most people just want access to the setColorScheme function, maybe i'll consider re-ordering for better ergonomics, or moving to an object for destructuring.