Category: Technical Commentary

Notes and opinions on technology — Linux, the web, AI, and software.

  • The React State Mistakes I Kept Making

    React state looks simple on the surface. You hold a value, you update it, the screen changes. In practice, I kept making the same mistakes for a while, and they all came from treating state like an ordinary variable. It is not. It is a snapshot of a render, and learning to respect that changed how I write components.

    My first mistake was mutating state. If state held a list, I would push an item into it and wonder why nothing updated. React compares references, not contents, and if I change the same array in place, React has no clear signal that anything new happened. The fix was to make a new array or a new object every time. It felt wasteful at first, but it is how React knows to re-render, and it keeps the old state intact for comparison.

    The second mistake was the stale closure. I would write an event handler or a timer that used a state value, and it would keep using the old value long after the state had changed. That happened because the function had closed over the value from the render when it was created. It was not broken, exactly. It was doing what JavaScript functions do. I just had not accounted for it.

    The habit that fixed stale values for me was to think about where the value comes from at the moment of the update. When the next value depends on the previous one, I use the updater form, which receives the latest state instead of relying on whatever the closure captured. For effects and timers, I check dependencies carefully and ask whether the function should be recreated when the value changes.

    The third mistake was lifting state too late. I would keep a piece of state deep inside a child component, then discover that a sibling also needed it, or that the parent needed to reset it. By then, the state was tangled up in the wrong place, and moving it meant rewiring several components. Now I ask earlier: who actually owns this value, and who else needs to read or change it? If more than one component cares, the state usually belongs higher up.

    Lifting state early is not the same as lifting everything to the top. I have also made that mistake, and it turns a parent into a dumping ground for values it does not really use. The balance I aim for now is simple: keep state as close as possible to where it is used, but no closer than the components that need to share it.

    Another habit that helped was to keep state minimal. If a value can be calculated from existing state, I do not store a second copy of it. Two copies will disagree eventually, usually at the worst moment. Derived values belong in the render, not in a separate state variable that has to be kept in sync by hand.

    The practical takeaway is a short checklist I now run through: am I creating a new value instead of mutating the old one, does this update depend on the latest state, and does this state live where the components that need it can reach it? Those three questions have fixed more React bugs for me than any clever trick, because they address how state actually works instead of how I wish it worked.

  • JSX Isn’t HTML, and That’s the Point

    When I first saw JSX, I thought it was just HTML moved into a JavaScript file. It looks like HTML, it reads a bit like HTML, and it even uses familiar tags. That first impression is useful, but it is also misleading. JSX is not HTML. It is JavaScript expressions wearing HTML’s clothes, and once I understood that, React started to make much more sense.

    In plain HTML, a page is mostly static. You write the markup, the browser shows it, and if you want it to change, you add script on top. In JSX, the markup is part of the program. You can put a variable inside curly braces, call a function, or use a condition to decide what appears. The result is not a separate template file that the code occasionally touches. It is code that describes what the interface should look like for the current data.

    That is powerful because it keeps the logic and the output close together. If a button should only appear when a user is signed in, the condition lives right where the button is described. If a list should show one item for each entry in an array, the mapping lives right there too. I do not have to hunt through a separate template, a separate script, and a separate set of rules to understand why something is on the screen.

    The traps for beginners are mostly naming traps, and they make perfect sense once you remember that JSX is JavaScript. The most famous one is class versus className. In HTML, you would write class to style an element. In JSX, class is already a word JavaScript uses, so React uses className instead. The first time I hit that, I stared at a style that was not applying and felt foolish. It was not a bug. It was me writing HTML in a place where JavaScript rules apply.

    The same idea explains other small differences. Attributes are written in camelCase, event handlers are functions rather than strings, and expressions go in curly braces. None of that is there to be difficult. It is there because JSX is being turned into JavaScript calls behind the scenes, and those calls follow JavaScript conventions.

    Thinking in components follows naturally from this. A component is a small, reusable piece of interface: a button, a card, a header, a form field. Each one takes in data, decides what to show, and returns JSX. When I break a page into components, I am not just tidying the code. I am giving each part of the interface a clear job, so a change in one place does not quietly break something somewhere else.

    I also learned to keep components honest. If a component needs a long list of conditions, tangled logic, and five different jobs, that is usually a sign it should be split up. JSX makes it tempting to embed everything in the markup, but just because you can put logic there does not mean all of it belongs there. The best JSX I write reads like a description: this is what shows, and here is the small piece of logic that decides it.

    The practical takeaway is to stop treating JSX as HTML with extra features. Treat it as JavaScript that describes an interface. When something surprises you, ask what JavaScript would do with that expression, not what HTML would do with that tag. That one shift in thinking turns most JSX confusion into a simple, checkable question.

  • JavaScript’s Event Loop Finally Clicked for Me

    For a long time, JavaScript’s event loop felt like a phrase people repeated without really explaining it. I could write code that worked, and I could copy an async pattern that worked, but I did not have a clear picture of what happened underneath. Then one day the model finally clicked, and a lot of confusing behaviour suddenly became predictable.

    The picture that helped me starts with the call stack. Think of it as a pile of work. When a function runs, it goes on top of the pile. When it finishes, it comes off. JavaScript handles one thing at a time on that pile, which is why a long, blocking loop can freeze a page. Nothing else can run until the current work is done.

    Next to the stack is the task queue, where callbacks wait their turn. When you set a timer, or when a network request finishes, the callback does not jump straight into the middle of running code. It waits in line. The event loop is the simple, patient mechanism that checks whether the stack is empty and, when it is, moves the next waiting callback onto it.

    Promises fit into this picture once I stopped treating them as magic. A promise is a value that is not ready yet, with a plan for what to do when it is. Its handlers are scheduled, not run instantly. That is why a promise that is already resolved can still feel slightly delayed: the handler has to wait for the current stack to clear, just like everything else.

    That ordering explains the classic surprises. Code after a promise still runs before the promise handler. A timer set for zero milliseconds does not run immediately; it waits for the stack and for anything already queued ahead of it. Once I saw timing as a queue rather than a clock, those examples stopped feeling like tricks and started feeling like consequences of a simple rule.

    The mental model also changed how I write async code. I try to keep individual steps small, so the stack clears regularly and the interface stays responsive. I avoid long synchronous work in the middle of a user interaction. And when I need a sequence of steps, I prefer readable async flow over nested callbacks, because I can follow the order of work in my head.

    I still get things wrong sometimes, especially around the finer details of microtasks and the exact order of mixed timers and promises. But I no longer guess blindly. I have a model I can test against: what is on the stack, what is waiting, and what the loop will do next.

    The practical takeaway is to learn the loop before you lean on async shortcuts. If you can picture the stack, the queue, and the loop moving work between them, promises stop being mysterious. They become a tool you can reason about, and async code becomes something you can predict instead of something you hope works.

    When I debug async code now, I start by asking a boring question: what is still running, and what is waiting? That question usually points to the answer faster than adding more logging everywhere. It also stops me from blaming the framework or the browser for what is really just an ordering problem I have not yet pictured clearly.

  • Hardening a Linux Server the Boring Way

    There is no exciting secret to hardening a Linux server. That is the good news, and also the part most people skip. The servers I have seen get into trouble were rarely beaten by some brilliant new attack. They were beaten by the basics that had been put off: an old package, a password that should never have been allowed, a port left open because nobody remembered why.

    I like boring security because boring security is repeatable. I can do it on a tired afternoon, I can check it later, and I can explain it to someone else without a diagram. If a security step only works when I am feeling clever, it is not really a security step. It is a hope.

    Start with updates. Timely updates are not glamorous, but they close known holes before they become someone else’s easy entry point. I keep a regular routine for checking and applying them, and I pay attention to services that need a restart after an update. An update that was downloaded but never applied does not protect anything.

    Next, stop using passwords for SSH if you can avoid it. Keys are harder to guess, harder to spray across thousands of servers, and easier to revoke when someone leaves or a laptop is lost. I disable password login for SSH, keep keys protected, and treat access like something to hand out carefully, not something to share around for convenience.

    A firewall is the next unglamorous layer. Only the ports that need to be reachable should be reachable. Everything else stays closed. That sounds obvious, yet I still find servers where a database or an admin panel is listening to the whole internet because it was convenient during setup and nobody went back to close it. Convenience during setup becomes risk for the life of the server.

    Fail2ban earns its place for the same reason. It does not make a server invincible, but it raises the cost of the constant background knocking that every public server receives. Combined with key-only SSH, it turns a noisy, automated guessing game into something much less rewarding for the attacker.

    Least privilege ties all of this together. Services should run with only the access they need. Users should have only the permissions their job requires. When something does go wrong, and eventually something will, a tight permission set limits how far the problem can spread. I would rather spend an extra minute setting a permission correctly than spend a weekend cleaning up after a service that could read and write everything.

    None of this is a one-time job. Hardening is a habit: check the basics, remove what is no longer needed, review who has access, and keep notes so the next version of you does not have to guess. The practical takeaway is simple and dull, which is exactly why it works: update promptly, use keys, close the ports you do not need, let fail2ban do its quiet work, and give nothing more permission than it needs.

    One more boring habit belongs on the list: know how you would recover. Backups, a tested restore, and a note about how the server is put together turn a bad day into a repair job instead of a rebuild from memory. Security is not only about keeping people out. It is about being able to put things right, calmly, when something slips through.

  • Why I Still Reach for the Terminal First

    I still reach for the terminal first. It is not nostalgia, and it is not a performance. When I need to know what a Linux server is actually doing, a black window with a prompt tells me faster, and more honestly, than almost anything else I have installed.

    That habit formed the practical way. I learned servers by fixing them, and fixing them usually meant reading what the machine itself had to say. A command either worked or it did not. A log line either explained the failure or pointed to the next place to look. There was no friendly green check mark smoothing over a problem that was still there underneath.

    Graphical panels have their place, and I use them. But they hide a lot by design. They show you the settings they think you need, in the order they think you need them. If a service is failing for a reason outside that tidy view, the panel may tell you everything is fine while the underlying process is stuck, out of memory, or pointed at the wrong configuration file. The terminal does not tidy things up for you. It shows you the error, the path, and the permission problem, all at once.

    My usual workflow is simple, and deliberately boring. I connect over SSH, I open tmux so a dropped connection does not kill the work I was doing, and I start with the basics: what is running, what changed recently, and what the logs say. From there I can check a service, look at disk space, test a connection, or read a configuration file without leaving the keyboard. It is one continuous line of thought, instead of a series of clicks through menus that each reset my attention.

    Logs are the best example of why this matters. In a terminal I can follow a log as it grows, filter it for the one error I care about, and compare it against what I was doing at the same moment. That feedback loop is fast. I try something, I see the result, I adjust. When I am troubleshooting, that speed is not a luxury. It is what keeps a small problem from turning into a long outage while I hunt through tabs.

    I am not saying a graphical interface is never better, because that would not be true. For browsing files visually, for looking at charts, or for handing a routine task to someone who does not live in a shell, a good interface wins. I also like a visual editor for writing and a browser for reading documentation. The point is not to use the terminal for everything. The point is to know when the terminal is the shortest honest path to the answer.

    If you are learning this work, my advice is to build a small terminal-first habit. Learn to connect safely, keep your sessions in tmux, read logs before you restart things, and write down the commands that actually fixed a problem. You do not need to memorize everything. You need to be comfortable asking the machine directly, and reading what it says back.

    The practical takeaway is simple: start at the terminal when something is wrong, and use a graphical tool when it genuinely saves time or makes the work clearer. That order has saved me more hours than any dashboard ever has, because it keeps me close to the truth of the system I am responsible for.