Achei uma maneira bem interessante de criar um useContext no Next@13 + Styled Components <= Custom Theme <= UseContext
E ai pessoa, tudo bem com vocês?
Entre uma treta e outra de código no frontend (web), vi que muita gente deixou de usar o styled components no Next por alguns motivos (principalmente por conta de erros de compilação, ou desempenho).
Eu gosto bastante de usar e funciona perfeitamente, inclusive com um Hook para temas dentro de um contexto customizado.
Vou colocar abaixo alguns blocos de código para ajudar no entendimento.
Obs: em alguns casos eu coloquei 3 pontos dentro de alguns dos blocos de código que separei para apenas demonstrar que ali você pode colocar outras coisas (a parte separada é demonstrativa). Não copie e cole sem ajustar.
Obs: Aqui a premissa básica é que você esteja usando todas as versões atualizadas até a data, ok?
"dependencies": {
"@next/font": "^13.0.5",
"@types/node": "18.11.9",
"@types/react": "18.0.25",
"@types/react-dom": "18.0.9",
"next": "13.0.5",
"nookies": "^2.5.2",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-icons": "^4.6.0",
"styled-components": "^5.3.6",
"typescript": "4.9.3"
...
},
"devDependencies": {
"@types/react-icons": "^3.0.0",
"@types/styled-components": "^5.1.26"
...
Primeiro, temos que definir o compilador no arquivo de config do Next. Isso não requer instalação de novos pacotes;
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
swcMinify: true,
i18n: {
locales: ['pt-BR'],
defaultLocale: 'pt-BR',
},
compiler: {
styledComponents: true,
},
}
module.exports = nextConfig
Em seguida, defina os types para o seu tema.
No meu caso, eu costumo declarar sobre o namespace para definir as propriedades (tipos) do tema (\src@types\styled-components\styles.d.ts):
Obs: Eu uso um dos temas definidos para extrair os types e aplicar sobre o módulo.
import 'styled-components'
import { Light } from '../../styles/themes'
type ThemeTypes = typeof Light
declare module 'styled-components' {
export interface DefaultTheme extends ThemeTypes {}
}
É necessário ajustar a _document.tsx para evitar alguns incómodos com a estilização:
class MyDocument extends Document {
static async getInitialProps(
ctx: DocumentContext,
): Promise<DocumentInitialProps> {
const sheet = new ServerStyleSheet()
const originalRenderPage = ctx.renderPage
try {
ctx.renderPage = () =>
originalRenderPage({
enhanceApp: (App) => (props) =>
sheet.collectStyles(<App {...props} />),
})
const initialProps = await Document.getInitialProps(ctx)
return {
...initialProps,
styles: (
<>
{initialProps.styles}
{sheet.getStyleElement()}
</>
),
}
} finally {
sheet.seal()
}
}
render() {
return (
<Html lang={this.props.locale}>
<Head>
<link rel="icon" href="/favicon.ico" />
...
</Head>
<body>
<Main />
<NextScript />
</body>
</Html>
)
}
}
export default MyDocument
A config do _app.tsx é padrão, porém é abaixo do componente "Head", já no bloco de return que definimos o provider do contexto que facilitará a abertura dos temas sobre a aplicação;
Bloco com o provedor de temas no _app.tsx:
...
</Head>
<AppThemeProvider>
<GlobalStyle />
<Component {...pageProps} />
<Analytics />
</AppThemeProvider>
</>
...
Por fim, vou jogar aqui minha config custom para uso do contexto sobre toda a aplicação.
Atenção quanto a um detalhe. Eu gerencio minha config com base em um seletor de cookies (nookies no caso). Lá é padrão, uma função para setar o cookie com o tema padrão no start, que resgata o cookie se já estiver setado (inclusive se já foi ajustado para outro tema pelo usuário, ele já faz com que a aplicação de start com o tema escolhido anteriormente), e uma outra função para atualizar o cookie com o novo tema. Isso inclusive que é o disparador que eu simplifiquei e que aparece abaixo na execução do LoadDefaultThemeCustom e toggleTheme:
import {
createContext,
ReactNode,
useContext,
useEffect,
useState,
} from 'react'
import { ThemeProvider } from 'styled-components'
import { Dark, Light } from '../../styles/themes'
import { LoadDefaultThemeCustom, UpdateCookieTheme } from '../../utils/theme'
interface IThemeContext {
toggleTheme(): void
applicationTheme: 'dark' | 'light'
}
const ThemeContext = createContext<IThemeContext>({} as IThemeContext)
ThemeContext.displayName = 'Theme'
type Props = {
children: ReactNode
}
export function AppThemeProvider({ children }: Props) {
const [applicationTheme, setApplicationTheme] = useState<'dark' | 'light'>(
'light',
)
function toggleTheme() {
const newTheme = applicationTheme === 'light' ? 'dark' : 'light'
UpdateCookieTheme(newTheme)
setApplicationTheme(newTheme)
}
useEffect(() => {
const defaultTheme = LoadDefaultThemeCustom()
const defined = defaultTheme.theme === 'light' ? 'light' : 'dark'
setApplicationTheme(defined)
}, [])
return (
<ThemeContext.Provider
value={{
toggleTheme,
applicationTheme,
}}
>
<ThemeProvider theme={applicationTheme === 'dark' ? Dark : Light}>
{children}
</ThemeProvider>
</ThemeContext.Provider>
)
}
export function useThemeHook() {
const { toggleTheme, applicationTheme } = useContext(ThemeContext)
return { toggleTheme, applicationTheme }
}
Daí pra frente, com o provider setado distribuindo o tema sobre a aplicação (que já é injetado no próprio hook nativo do SC), só definir um lugar para fazer a alteração dos temas.
No meu caso, usei a função inserida em um botão no meu header, mas você pode colocar isso em qualquer lugar.
Inclusive como eu estou resgatando o tema atual da aplicação, você pode inclusive usar ele para fazer transição de outros objetos, como por exemplo, imagens em svg para mudar de cor (fiz isso inclusive com um ícone):
...
import { useThemeHook } from '../../hooks/contexts/AppThemeContexts'
function HomeHeader() {
const { toggleTheme, applicationTheme } = useThemeHook()
const definedTheme = useMemo(
() => (applicationTheme === 'dark' ? themeLight : themerDark),
[applicationTheme],
)
return (
<Container>
<ButtonThemeSet onClick={toggleTheme}>
<Image src={definedTheme} alt="Texto da imagem" />
</ButtonThemeSet>
<MenuContainer>
...
E é isso!
Se olhar este post e tiver sugestões que podem melhorar a ideia, fique a vontade para comentar.