@userkit/react
v0.4.0
Published
Drop-in UserKit components for React: <SignIn />, <SignUp />, <UserButton />, and the hooks under them.
Maintainers
Readme
@userkit/react
Os componentes prontos do UserKit para React, e os hooks embaixo deles.
npm install @userkit/reactVersão
0.3.0, acompanhando o@userkit/js. Os três pacotes são versionados juntos: um@userkit/react0.2.x sempre pede o@userkit/js0.2.x.
Uso
import { UserKitProvider, SignIn, SignedIn, SignedOut, UserButton } from "@userkit/react";
export default function App() {
return (
<UserKitProvider publishableKey="uk_pk_live_…">
<header>
<SignedIn><UserButton /></SignedIn>
</header>
<SignedOut><SignIn /></SignedOut>
</UserKitProvider>
);
}É isso. O <SignIn /> pergunta à API o que aquele ambiente suporta — então
ligar o Google no painel acende o botão sem ninguém fazer deploy — e a página de
callback do login social é a mesma página, porque o provider termina o fluxo
sozinho quando encontra um ?code= que ele mesmo iniciou.
Os guardas
<SessionLoading><Skeleton /></SessionLoading>
<SignedIn><App /></SignedIn>
<SignedOut><SignIn /></SignedOut>
<Verified><Faturas /></Verified>Nenhum dos dois primeiros renderiza enquanto a sessão está sendo restaurada. É para isso que existe o terceiro estado: uma página que mostra "deslogado" por um frame a cada recarga é o defeito mais visível que uma biblioteca de autenticação pode ter — e ele é invisível numa conexão rápida durante o desenvolvimento.
<Verified> é para sessão com identidade provada. Ela é cortesia; quem
impede de verdade é o requireVerifiedContact do outro lado.
Os hooks
const { client, state, config } = useUserKitContext();
const contact = useContact();
const session = useSession();Os componentes são conveniência sobre os hooks, não uma parede na frente deles.
No momento em que o <SignIn /> não é o formato que você quer, você escreve o
formulário e mantém todo o resto.
A conta e os times
<header>
<SignedIn>
<CustomerSwitcher onSwitch={(customer) => router.push(`/${customer.id}`)} />
<UserButton />
</SignedIn>
</header>
<SignedIn><AccountSettings /></SignedIn>O <CustomerSwitcher /> não escreve nada ao trocar de time: ele muda o
X-Customer-Id que vai nas chamadas seguintes, e a associação por trás do header
é quem autoriza. Criar um time é a única escrita que ele tem.
Sobre o nome: o que ele troca é um customer — a entidade que paga, um customer
com vários contatos — e o componente se chama pelo que troca, como tudo que ele
toca (o callback recebe um CustomerMembership). OrganizationSwitcher e
OrganizationProfile existem como alias do mesmo componente, porque é a
palavra que quem vem de outro SDK digita quando vai procurar por ele.
O <AccountSettings /> renderiza o que o ambiente e a sessão realmente podem
fazer: num ambiente federado não aparece senha nem verificação em duas etapas —
a API responde 409 nas duas —, e o que está atrás do requireVerifiedContact
(dispositivos, segundo fator, equipe) só aparece para sessão com identidade
provada.
Flags e engajamento
<Flag name="new_checkout" fallback={<CheckoutAntigo />}>
<CheckoutNovo />
</Flag>
const ligada = useFlag("new_checkout"); // boolean, nunca null, nunca promiseuseFlag devolve um booleano e ponto: um hook que começasse em undefined
faria todo recurso com flag renderizar sua ausência por um frame e aparecer no
seguinte — o mesmo defeito que o <SessionLoading> existe para evitar uma
camada acima, só que aqui não há terceiro estado para desenhar, porque "ainda
não sei" e "desligado" são idênticos para a pessoa e só um dos dois é seguro.
<Flag> é cortesia como o <Verified>: quem impede de verdade é o servidor.
<OnboardingChecklist />
<ChangelogBadge />
<Survey />
<FeedbackButton />Cada um renderiza o que o ambiente consegue responder e nada além. O checklist
não tem botão que marque um passo — não existe endpoint que faça isso, porque um
passo é satisfeito por um fato ter acontecido ou por um direito do plano estar
valendo. O <Survey /> não tem prop que peça uma pesquisa: quem decide quem é
perguntado é a API — o que ele tem é position: center (padrão), bottom-right,
bottom-left ou inline. Só o centro escurece a página e toma o foco; os cantos
flutuam sem backdrop, porque escurecer o produto para perguntar sobre o produto é o
que faz um NPS de cinco segundos parecer um pedágio. Esc e o clique fora são "agora
não" e não gastam a entrega.
O <FeedbackButton /> aceita as mesmas quatro posições, com inline de padrão —
e nas três flutuantes o que fica no seu layout é só o botão, porque um header tem
espaço para um botão e não para uma lista de ideias com um formulário embaixo. E abrir o <ChangelogBadge /> é o que marca como lidos os
posts que ele de fato mostrou — a API recusa um post que o leitor nunca viu, em
vez de deixar um badge ser zerado por uuid.
<WhatsNew />O mesmo changelog pelo outro lado. O badge espera ser clicado; este abre
sozinho — "Enquanto você esteve fora" — com o que aquele contato não viu, e
não desenha nada quando não há nada. É a ordem das duas perguntas que
garante isso: ele lê o contador primeiro e, num zero, acaba ali — a lista nem
chega a ser pedida. O conjunto que ele mostra é o do próprio contador: não lido
e publicado depois de since, que é quando aquele contato chegou. Quem
chegou hoje não esteve fora de nada, e vê o diálogo que isso merece, que é
nenhum.
Ser mostrado é o que marca esses posts como lidos, senão o mesmo diálogo
cumprimenta a mesma pessoa amanhã com os mesmos posts. E o corpo aqui é
markdown de verdade, via <HelpMarkdown /> — que constrói elementos e nunca
HTML, então a prosa do inquilino chegando pela rede não tem onde injetar.
Todas as frases passam pelo dicionário (localization), incluindo os rótulos
acessíveis: um checklist lido em voz alta precisa dizer quais passos estão
concluídos, e cor sozinha não é um estado.
As notificações dentro do produto
<NotificationBell />
const { notifications, unread, markRead, markAllRead } = useNotifications();O que aparece aqui é o que o seu backend endereçou a esta pessoa sobre o
trabalho dela — "seu export está pronto", "a fatura falhou" — escrito em
POST /v1/notifications com a chave uk_sk_. Não há metade que componha uma:
uma rota alcançável pelo navegador deixaria qualquer página dizer qualquer coisa
a qualquer usuário seu.
Abrir o sino não marca nada como lido, e essa é a diferença para o
<ChangelogBadge />. Um post de changelog é um anúncio que todo mundo recebe, e
abrir a lista é honestamente o ato de ler; uma notificação é endereçada a uma
pessoa sobre uma coisa, e zerar dez porque alguém olhou um dropdown apaga
exatamente o estado que ela veio conferir. Cada linha é marcada quando é
acionada, e "marcar todas" é um botão que se aperta de propósito.
O painel abre como uma gaveta lateral, num portal para fora da árvore do
componente: o sino costuma morar num header sticky, e um menu absoluto ali
dentro é recortado por um overflow que este componente não enxerga.
Uma notificação pode levar a algum lugar — url, escrito pelo seu backend junto
com o título. A linha vira link, e numa SPA o onNavigate entrega o caminho ao
seu roteador em vez de recarregar a página:
<NotificationBell onNavigate={(url) => router.push(url)} />Um endereço de outra origem — o seu segundo domínio — abre em aba nova com
rel="noopener noreferrer" e nunca passa pelo onNavigate. Um clique com
modificador fica com o navegador, então o botão do meio continua abrindo aba.
O poll roda só com a aba visível — um minuto por padrão, e nada enquanto a
página está escondida: /v1/contact/* dá 600 requisições por hora por sessão, e
uma aba de fundo perguntando para sempre é bateria de terceiros gasta num badge
que ninguém está olhando. Quando a aba volta, ele pergunta na hora em vez de
esperar o intervalo que dormiu.
Aparência
<SignIn
appearance={{
logoUrl: "/logo.svg",
variables: { colorPrimary: "#5b21b6", borderRadius: "0.75rem" },
}}
/>A lista de tokens é curta de propósito: estes componentes renderizam na página
do desenvolvedor, então o que importa é o punhado de variáveis que faz eles
pertencerem ali — não um segundo design system embarcado numa biblioteca de
auth. Além disso, appearance={{ unstyled: true }} não injeta folha de estilo
nenhuma e deixa os nomes de classe (uk-*) para o seu CSS.
O tema escuro segue o prefers-color-scheme por padrão.
O que não está aqui
Nada aqui edita nome, e-mail ou avatar: /v1/contact/* não tem rota que faça
isso, e um componente para uma superfície que a API não tem é promessa, não
funcionalidade. Trocar a senha, pela mesma razão, é um link enviado para o
e-mail — é o único caminho que existe, e é ele que prova o endereço.
Estilo, tema e CSP
A folha de estilo é injetada pelo pacote — não há .css para importar. Em
React 19 ela vai como <style href precedence>: o React a sobe para o <head>,
deduplica entre todos os componentes e a emite no servidor, então uma página
renderizada no Next chega com o formulário já estilizado. Em React 18 não existe
hoisting: a folha entra por JavaScript no primeiro render do cliente, e o primeiro
paint de uma página SSR é um formulário sem estilo até a hidratação.
Os tokens (--uk-color-primary, --uk-radius, …) têm o padrão claro na própria
folha e o escuro num bloco prefers-color-scheme: dark. O elemento só carrega os
tokens que alguém definiu — o appearance.variables ou a paleta do tenant —
e por isso o modo escuro funciona: um valor inline vence qualquer regra, e um
padrão inline seria um bloco escuro que nunca se aplica.
<SignIn appearance={{ theme: "dark" }} /> // decide aqui, ignora o sistema
<SignIn appearance={{ theme: "light" }} />
<SignIn appearance={{ variables: { colorPrimary: "#0b5cff" } }} />appearance.theme escreve data-uk-theme na raiz — é o que um app com o tema
numa classe do <html> (Tailwind, shadcn) precisa, porque o sistema pode dizer
uma coisa e o botão no cabeçalho outra.
unstyled é por página, não por componente: a folha é uma só, e as classes
são as mesmas. Um <SignIn appearance={{ unstyled: true }} /> ao lado de um
<UserButton /> estilizado continua recebendo as regras que o outro trouxe.
Content-Security-Policy
Uma style-src sem 'unsafe-inline' bloqueia a folha em silêncio. Passe o nonce
da página:
<UserKitProvider publishableKey="…" nonce={nonce}>No cliente ele vai para todo <style> que o pacote escreve. No servidor o React
descarta o atributo de um <style> hoisted e aplica a opção do próprio render —
nonce: { style } em renderToPipeableStream/renderToReadableStream (React
19.1+), que o Next preenche a partir do cabeçalho CSP da página. Fora do React,
setMountTarget({ nonce }) é a mesma configuração.
Avançado
UserKitContexté exportado para um teste que quer entregar um valor pronto aos componentes. Em código de produto useuseUserKitContext(), que lança fora de um provider em vez de devolvernull.@userkit/react/localesentregaptBReenUScomo dados, sem a diretiva"use client"— para um Server Component que quer ler uma frase.useOnboardingGuide(),useHelpWidget(),useNotificationBell()eusePoints()são o estado dos drop-ins, para um botão seu.Positioné o vocabulário de posição dos flutuantes (center,bottom-right,bottom-center,bottom-left,inline);Position,PositionePositionsão o mesmo tipo com os nomes que já saíram.- O pacote é ESM-only: não há
require. Jest em CJS puro precisa detransformIgnorePatternspara@userkit/. - As props do
<UserKitProvider>são um tipo largo, não uma união das três formas (publishableKey,mode: "proxy",client): o@userkit/nextjsenvolve o componente e espalha as próprias props, e uma união discriminada não sobrevive a umOmitmais um spread. Faltar as três é um erro em runtime, na construção do client.
