Firstly, I wouldn't worry about tracking board state history, computers are powerful enough that modern frameworks like React, can afford to basically store the entire history of your interaction with a website, just because it makes debugging and writing tests way easier.
But if you really want to, here's how I'd do it efficiently:
You don't need to store the entire board, since it's mostly empty. It can have a max 24 occupied positions out of 64, so you only need to store those. The only other thing you have to worry about, is whether the king can castle(because technically, 2 identical board states are different for the purpose of the draw rules, if the king can castle in one and not the other).
So you have a board position, represented by an unsigned short, of which bit 0 is black/white, 1-3 is row, 4-6 is column and 7-9 is piece_type.
You have a board struct
unsigned short positions;
The positions are stored in the order(doesn't matter, it just has to be consistent, so they can be compared) of Ascending row and Ascending column within row. And to efficiently compare 2 boards: check if num_positions is the same, if not(one piece has taken another) return false, loop through positions, if board1.positions[n] != board2.positions, return false, return true.
Another way to do it, would be to have a struct with:
unsigned short pos1,
unsigned short pos2,
unsigned short pos24
less space efficient, but equality comparison is simply board1 ^(bitwise XOR) board2 > 0
Then, for the game, you have a Stack, that you push each new board state onto as a player makes a move. i.e.
If something happens that obviously invalidates the 3 repetition rule, like a piece being taken, promoted, or castling no longer possible, toss the old boardHistory, i.e.
boardHistory = new Stack<BoardHistory>(latestMove)
When you search for repetitions, you only have to search a small stack for two repetitions, because it will get cleared repeatedly throughout the game and the board comparison is very efficient as well.