
Frameworks vs. Fundamentals: How overreliance on tools can stunt your growth as a developer
Introduction: The Power of Fundamentals
While building with modern tools like Next.js dominates my current projects, I've been exploring functional programming concepts like monads. But beyond the usual web development, I occasionally take some time to study other topics. The current hyperfocus is functional programming and its infamous monads. You might ask, “Why is this important to you, David?”, “Are monads paying well?” Honestly, I doubt it, but in the programming world, you never know. However, even if functional programming isn't the hot topic or the most talked about trend right now, it brings fundamental concepts that are essential for any developer. By understanding the basics and applying these principles in different contexts, I can improve the quality of my code, whether in new projects or legacy systems. The Trap of "Magical" Stacks. The tech world obsesses over "magic" stacks (Next.js, React, Tailwind) and new tools emerging daily (while you read this a new js framework arrives on github). But hyper focusing on trends risks creating brittle expertise. Without core programming principles, we’re just stacking Lego blocks without understanding the bricks. Imagine inheriting a good and old legacy system, some hacks some code pieces glued by silver tape. A developer reliant solely on modern abstractions would drown. Functional concepts like immutability or pure functions aren’t academic—they’re survival skills for untangling spaghetti code, regardless of the stack. Frameworks come and go, but clean architecture, modular design, and problem solving logic endure. Investing in fundamentals isn’t about chasing hype—it’s about building adaptable expertise that outlives today’s “ultra-powerful master blaster” tools.
Example 1: Modern Project with React Hooks
Here’s a simple example of a to-do list application (the classic "todo list") using React and Next.js:
1"use client";
2import { useState, useEffect } from 'react';
3
4export default function TodoApp() {
5 const [todos, setTodos] = useState([]);
6
7 useEffect(() => {
8 fetch('/api/todos')
9 .then(response => response.json())
10 .then(data => setTodos(data))
11 .catch(error => console.error('Error fetching todos:', error));
12 }, []);
13
14 const addTodo = (newTodo) => {
15 setTodos((prevTodos) => [...prevTodos, newTodo]);
16 };
17
18 return (
19 <div>
20 <h1>My Todo List</h1>
21 <ul>
22 {todos.map((todo, index) => (
23 <li key={index}>{todo}</li>
24 ))}
25 </ul>
26 <button onClick={() => addTodo('New Task')}>Add Task</button>
27 </div>
28 );
29}
30
Modern Approach: The use of hooks like useState and useEffect enables reactive and well structured state management. Immutability and Best Practices: The state is updated immutably, which is a practice derived from fundamental concepts.
This code is elegant and efficient, but without understanding concepts like immutability, pure functions, and state management, you might just be following a "recipe" without understanding the "why" behind each decision.
Example 2: Legacy Project with Imperative Manipulation
Now, see how an older, more legacy codebase without modern abstractions might approach the same problem:
1<!DOCTYPE html>
2<html>
3 <body>
4 <div id="app"></div>
5 <script>
6 function TodoApp(container) {
7 let todos = [];
8 function fetchTodos(callback) {
9 fetch('/api/todos')
10 .then(response => response.json())
11 .then(data => callback(data))
12 .catch(error => console.error('Error fetching todos:', error));
13 }
14 function render() {
15 const todoList = container.querySelector('#todo-list');
16 todoList.innerHTML = '';
17 todos.forEach((todo) => {
18 const li = document.createElement('li');
19 li.textContent = todo;
20 todoList.appendChild(li);
21 });
22 }
23 function addTodo(newTodo) {
24 todos.push(newTodo);
25 render();
26 }
27 container.innerHTML = `
28 <h1>My Todo List</h1>
29 <ul id="todo-list"></ul>
30 <button id="add-task">Add Task</button>
31 `;
32 container.querySelector('#add-task').addEventListener('click', () => {
33 addTodo('New Task');
34 });
35 fetchTodos((data) => {
36 todos = data;
37 render();
38 });
39 }
40 TodoApp(document.getElementById('app'));
41 </script>
42 </body>
43</html>
44
Legacy Approach: Here, the todos state is directly manipulated, and the rendering logic is manually controlled.
Legacy Challenges: The lack of abstractions makes the code harder to maintain and refactor. However, understanding the imperative logic behind it can be essential for those who need to support older systems.
Comparison between the two examples:
Modern Code: It’s more readable and takes advantage of useful abstractions but can mask the internal workings for those who rely solely on frameworks.
Legacy Code: It requires more effort to understand and work with but offers a more direct view of the fundamental processes.
Balancing the Modern and the Classic
A developer's true skill lies not in clinging to one technology but in navigating confidently between different paradigms and coding styles. For this:
Understand the Concepts Behind the Tools: Why do we use hooks? How does state management work in different paradigms?
Practice with Diversity: Work with modern frameworks but also explore different languages or contexts.
Learn from Legacy: Legacy projects are a goldmine for learning because they expose challenges that modern tools try to abstract away.
Conclusion: Master the Present, Study the Past
Legacy projects often present challenges that go beyond the documentation of a modern framework. They demand a deeper understanding of how things work under the hood. On the other hand, modern projects, even with all their sophistication, also benefit greatly from a solid foundation.Frameworks like Next.js aren't enemies; they're powerful allies when used wisely. However, to truly master programming, it’s essential to go beyond the tools and explore the concepts that underpin them. Programming isn’t about choosing between the modern and the classic. It’s about finding strength in balance, understanding that fundamentals are the roots of everything we build, whether it’s a modern application or maintaining a legacy system.