Перейти к содержимому

Блог

When a backend developer tells you to parse a JWT token for user information...

I hadn’t parsed a JWT token on the client before, but this week our backender said that he didn’t have time to create a new endpoint like /user, and that I should extract user information from the JWT token in a cookie.

My first thought was: is this okay? On all projects that I’ve seen backend gave me endpoint like /user, /me, /current — and these endpoints were created for a reason, weren’t they?

Yes, there are reasons:

  • you cannot store sensitive, confidential information in JWT payload
  • you should be mindful of the size of the JWT (it is transmitted with cookies in every request)

For example, it is not a good idea to store in JWT permissions for user and rely on it in your UI. Because someone can modify the token, and your application could be at risk.

However, this means you can use a JWT to store some basic user information. In my current situation, I only need name, email and photo of the authorized user, so I started parsing JWT for this data and backend developer took another task.

What do you need for parsing JWT on client?

JWTs are Base64 encoded and contain of three parts: header, payload, and signature. You can write your own decoding function or use one of the ready-made solutions. I checked what is there on the internet for handling JWT tokens on the client-side:

  • jsonwebtoken — very popular library for Node.js. It is used for generating and verifying JWTs on the server-side, can be used on the client side. It’s a good library, but overkill for my case.
  • jose — another library for implementing JWT, it provides functionality for signing and verifying tokens and set of utility functions. Again: good, but overkill for my case.
  • jwt-decode — lightweight library only for decoding tokens. Single-purpose, easy to use, zero dependencies — that’s the winner for today.

First impressions of using CursorAI

I’ve been using Cursor for 3 weeks, and I’m really impressed. Of course, it is not ideal and it doesn’t replace a human developer (for now), but it can help in a lot of ways.

My favorite uses for it are writing tests with instructions, creating TS types, and drawing diagrams. Let’s look closer at each case.

Writing tests

Not all developers love to write unit and integration tests. I personally lose my inspiration when I need to create a lot of mocks, do some routine tasks for preparing the test environment and try to make everything work with components or other libraries.

And that’s the place where AI shines! Of course not without a helping hand, but…

How I improved results of generation

  • created mdx files with instructions for writing tests with examples (separate files for unit and integration tests)
  • described the flow: AI ought to write tests, after that run them and check results. If there are failed tests, AI fixes them
  • run command write tests for ... and add relevant instructions to the context

Of course, there is still a lot of work with reviewing, but with mocks and setting up environments it helps a lot.

Generating types

When you get a new endpoint and add it to your application, you need to describe types. In all our projects we use TypeScript, and before starting to use AI I created types manually (there is Swagger only on some of the projects). But now… I just need to give the API response to AI and describe what it is and some constraints — and after a few minutes I have generated types. There is room for improvement, but it speeds me up a lot.

Diagrams

I love diagrams, and I believe that it is easier to understand processes and technical details from diagrams and schemes, not from plain text. And I explored a whole new world for me with this prompt: @project explain how is ___ implemented and draw a scheme of how ____ works. I can read and have a mermaid diagram (I just copy and paste in an online mermaid editor) in front of my eyes — it has simplified a lot.

Tag b or not b...

I’ve been refactoring our codebase, and it seems like my colleagues from the past were very fond of the <b> tag. They’ve used it a lot. I am not a big fan, especially of code like this:

.component b {
font-weight: normal;
}

But maybe there is some reason why these guys applied it everywhere? Let’s research a little!

<b> now

I knew that <b> was for Bold. As I found out, now this is not the case. The HTML5 specification says:

The b element represents a span of text to which attention is being drawn for utilitarian purposes without conveying any extra importance and with no implication of an alternate voice or mood, such as key words in a document abstract, product names in a review, actionable words in interactive text-driven software, or an article lede.

And adds:

The b element should be used as a last resort when no other element is more appropriate.

For me, it is a really confusing tag now: there are some ideas about appearance (and browsers support it), but <b> is not about appearance. Issues with this tag are presented in the W3C article “Using <b> and <i> elements”.

I plan to get rid of <b> in most places of my code and try to use h1-h6, em, strong or mark — a pretty wide choice!

Когда ре-рендер нужен, но никак не случается

Я уже сталкивалась с этим и уже удивлялась этому поведению, но позабыла — и поэтому сегодня потратила на отладку пару часов: подозревала в коварстве библиотеку компонентов, которую использую, искала проблемы в композиции и отлаживала изменения контекста.

Итак, что было? В нашем App.tsx:

<Router history={history}>
<UuiContext.Provider value={services}>
<Header />
<Routes />
<Footer />
</UuiContext.Provider>
</Router>

В Header.tsx — навигация:

// компонент из библиотеки, у него есть подсветка активного пункта меню.
// заглядывает в uuiContext от UuiContext.Provider и проверят, активная ли линка через history
<MainMenu>
<MainMenuButton caption='Home' link={{pathname: '/' } />
<MainMenuButton caption='Dashboard' link={{pathname: '/dashboard' } />
</MainMenu>

И вроде всё (ну, почти всё) работает: по кликам в MainMenu урл обновляется, контент перерисовывается. Вот только нет выделения активного элемента в MainMenu.

Начинаю разбираться, почему — ставлю точку остановы в фукнцию в MainMenu.tsx, где определяется, активная ли линка. И тут начинается интересное: при переходе по кнопкам и обновлении урла функция не вызывается.

Добавляю console.log(‘render’) в Header.tsx. Что ж, он не пере-рендеривается, так что логично, что ничего не происходит. Но ведь мы же вроде обновляем историю, а в uuiContext.uuiRouter.history — как раз всё обновляется… Почему же дочерние элементы UuiContext.Provider не обновляются?

Тут я пошла неверным путём: попыталась отладиться в контекстах, потому что заподозрила, что у меня где-то создается несколько инстансов и происходит путаница. Ничего не нашла, естественно.

После блуждания по контекстам, в мою голову пришла мысль: содержимое страницы обновляется за счет изменения актуального Route — поэтому часть в Routes перерендеривается нормально. Что, если Header положить внутрь каждой страницы, соответствующей роуту? Это помогло, однако в таком случае Header каждый раз при изменении роута unmount и mount. Выглядит некрасиво и излишне.

И вот тут наконец хороший вопрос пришел в голову: как React Router определяет, что надо что-то пере-рендерить? И вот что я выяснила:

  • мы обновляем history
  • history object имеет постоянную ссылку
  • useHistory поэтому все время будет возвращать один и тот же объект, поэтому useEffect c history.location не отработает при изменении location
  • зато вот useLocation и useParams следят за изменениями локации, поэтому как только мы их используем — наш компонент начинает пере-рендериваться при изменениях

Самое противное, что у меня уже была подобная проблема, но вспомнила о ней и об этом моменте я только тогда, когда “заново” нашла решение. Записываю сюда, чтобы больше не забывать 🤪

Миф про ре-рендеры

Вам когда-нибудь говорили, что вот так передавать массив — плохая практика?

<Component levels={[1, 2, 3]} />

Ведь массив будет пересоздаваться в JSX при каждом ре-рендере, то есть пропсы будут меняться и запускать ненужные ре-рендеры Component?

И вы наверняка слышали, что нужно использовать useMemo (const levels = useMemo(() => [1, 2, 3], [])) или объявить массив вне компонента?

Я слышала это много раз. И следовала этому принципу.

Даже когда я столкнулась с кейсами, что при передаче в пропсы ref объекта и обновлении этого объекта потом — компонент не перерисовывается, я всё равно продолжала избегать передачи вот таких массивов и объектов как пропсов. Я просто не соединила в своей голове эти две простые вещи: если обновление пропсов должно приводить к ре-рендеру, тогда это должно работать и с ref, не так ли?

Сейчас я читалю Advanced React от Nadia Makarevich, и автор пишет, что одно из главных заблуждений:

Миф про ре-рендеры: Компонент перерисовывается, когда изменились пропсы.

React обновляется, когда происходит обновление стейта. И в этом случае, он перерендерит все вложенные компоненты, изменились их пропсы или нет.

В контексте ре-рендеров, изменились пропсы компонента или нет важно только в одном случае: если этот компонент обернут в HOC React.memo.

Посмотрите на примере с кодом: https://codesandbox.io/p/sandbox/props-rerender-dcq97c.

И всё! Нужно поменять способ мышления — перенести фокус с изменения пропсов на анализ изменений стейта.

P.S. Сама я для константных объектов / массивов предпочитаю создавать constants.ts и объявлять их там, но это дело личного вкуса и никак не связано с ре-рендерами.