DEV Community

Professional Joe
Professional Joe

Posted on

Juris.JS: Dynamic Path Builders: Scaling State Management in Juris Applications

The Problem with String-Based State Paths

As Juris applications grow in complexity, managing state paths becomes a significant challenge. While simple string paths work well for small projects:

context.setState('user.profile.name', 'John');
context.setState('api.users.loading', true);
Enter fullscreen mode Exit fullscreen mode

Large applications quickly run into maintainability issues:

  • No type safety - Typos cause silent bugs
  • Hard to refactor - Changing paths requires manual search/replace
  • Inconsistent naming - Different developers use different conventions
  • No discoverability - No way to know what state paths exist

Evolution of State Path Management

Stage 1: Raw Strings (Small Apps)

// Simple but error-prone
context.setState('user.loading', true);
context.getState('api.error', null);
Enter fullscreen mode Exit fullscreen mode

Stage 2: Static Constants (Medium Apps)

// Better maintainability
const PATHS = {
    USER_LOADING: 'user.loading',
    API_ERROR: 'api.error'
};

context.setState(PATHS.USER_LOADING, true);
Enter fullscreen mode Exit fullscreen mode

Stage 3: Dynamic Path Builders (Large Apps)

// Flexible and scalable
const PATHS = {
    userById: (id) => `users.${id}`,
    apiStatus: (endpoint) => `api.${endpoint}.status`
};

context.setState(PATHS.userById(123), userData);
Enter fullscreen mode Exit fullscreen mode

Introducing Dynamic Path Builders

Dynamic path builders are functions that generate state paths based on parameters. They combine the flexibility of string paths with the safety and maintainability of constants.

Basic Pattern

export const PATHS = {
    // Static paths
    AUTH_TOKEN: 'auth.token',

    // Dynamic builders
    userById: (id) => `users.${id}`,
    formField: (form, field) => `forms.${form}.${field}`,
    apiEndpoint: (endpoint, property) => `api.${endpoint}.${property}`
};
Enter fullscreen mode Exit fullscreen mode

Usage Examples

// User management
context.setState(PATHS.userById(123), { name: 'John', email: 'john@example.com' });
context.getState(PATHS.userById(456), null);

// Form handling
context.setState(PATHS.formField('login', 'email'), 'user@example.com');
context.setState(PATHS.formField('registration', 'password'), '••••••••');

// API state management
context.setState(PATHS.apiEndpoint('users', 'loading'), true);
context.setState(PATHS.apiEndpoint('orders', 'error'), 'Network timeout');
Enter fullscreen mode Exit fullscreen mode

Advanced Patterns

Nested Path Builders

export const PATHS = {
    user: {
        profile: (userId) => `users.${userId}.profile`,
        preferences: (userId) => `users.${userId}.preferences`,
        activity: (userId, type) => `users.${userId}.activity.${type}`
    },

    api: {
        loading: (endpoint) => `api.${endpoint}.loading`,
        error: (endpoint) => `api.${endpoint}.error`,
        data: (endpoint) => `api.${endpoint}.data`,
        cache: (endpoint, params) => `api.${endpoint}.cache.${btoa(JSON.stringify(params))}`
    }
};

// Usage
context.setState(PATHS.user.profile(123), profileData);
context.setState(PATHS.api.loading('users'), true);
Enter fullscreen mode Exit fullscreen mode

Validation and Type Safety

export const PATHS = {
    userById: (id) => {
        if (!id || typeof id !== 'number') {
            throw new Error('User ID must be a number');
        }
        return `users.${id}`;
    },

    formField: (form, field) => {
        const validForms = ['login', 'registration', 'profile'];
        if (!validForms.includes(form)) {
            throw new Error(`Invalid form: ${form}`);
        }
        return `forms.${form}.${field}`;
    }
};
Enter fullscreen mode Exit fullscreen mode

Path Templates with Validation

const createPathBuilder = (template, validators = {}) => {
    return (...args) => {
        // Validate arguments
        Object.entries(validators).forEach(([index, validator]) => {
            if (!validator(args[index])) {
                throw new Error(`Invalid argument at position ${index}`);
            }
        });

        // Replace placeholders
        return template.replace(/\{(\d+)\}/g, (match, index) => args[index]);
    };
};

export const PATHS = {
    userById: createPathBuilder('users.{0}', {
        0: (id) => typeof id === 'number' && id > 0
    }),

    apiResource: createPathBuilder('api.{0}.{1}', {
        0: (resource) => typeof resource === 'string',
        1: (property) => ['loading', 'error', 'data'].includes(property)
    })
};
Enter fullscreen mode Exit fullscreen mode

Comparison with Other State Management Approaches

Redux/Toolkit Comparison

// Redux actions (traditional)
const SET_USER = 'users/setUser';
const SET_LOADING = 'api/setLoading';

// Juris dynamic paths
const PATHS = {
    userById: (id) => `users.${id}`,
    apiLoading: (endpoint) => `api.${endpoint}.loading`
};
Enter fullscreen mode Exit fullscreen mode

Zustand Comparison

// Zustand stores
const useUserStore = create((set) => ({
    users: {},
    setUser: (id, data) => set((state) => ({
        users: { ...state.users, [id]: data }
    }))
}));

// Juris approach
context.setState(PATHS.userById(id), data);
Enter fullscreen mode Exit fullscreen mode

Jotai Atoms Comparison

// Jotai atoms
const userAtom = (id) => atom(null);

// Juris paths
const userById = (id) => `users.${id}`;
Enter fullscreen mode Exit fullscreen mode

Performance Considerations

Path Caching

const pathCache = new Map();

export const PATHS = {
    userById: (id) => {
        if (!pathCache.has(`user-${id}`)) {
            pathCache.set(`user-${id}`, `users.${id}`);
        }
        return pathCache.get(`user-${id}`);
    }
};
Enter fullscreen mode Exit fullscreen mode

Lazy Path Generation

export const PATHS = {
    get userPaths() {
        return {
            byId: (id) => `users.${id}`,
            profile: (id) => `users.${id}.profile`,
            settings: (id) => `users.${id}.settings`
        };
    }
};
Enter fullscreen mode Exit fullscreen mode

Memory Optimization

// Avoid creating new functions on each access
const createUserPath = (id) => `users.${id}`;
const createApiPath = (endpoint, prop) => `api.${endpoint}.${prop}`;

export const PATHS = {
    userById: createUserPath,
    apiProperty: createApiPath
};
Enter fullscreen mode Exit fullscreen mode

Best Practices

1. Consistent Naming Convention

// ✅ Good: Consistent verb naming
export const PATHS = {
    getUserById: (id) => `users.${id}`,
    getApiStatus: (endpoint) => `api.${endpoint}.status`,
    getFormField: (form, field) => `forms.${form}.${field}`
};

// ❌ Avoid: Inconsistent naming
export const PATHS = {
    user: (id) => `users.${id}`,
    apiStatusFor: (endpoint) => `api.${endpoint}.status`,
    formFieldValue: (form, field) => `forms.${form}.${field}`
};
Enter fullscreen mode Exit fullscreen mode

2. Group Related Paths

export const USER_PATHS = {
    byId: (id) => `users.${id}`,
    profile: (id) => `users.${id}.profile`,
    preferences: (id) => `users.${id}.preferences`
};

export const API_PATHS = {
    loading: (endpoint) => `api.${endpoint}.loading`,
    error: (endpoint) => `api.${endpoint}.error`,
    data: (endpoint) => `api.${endpoint}.data`
};
Enter fullscreen mode Exit fullscreen mode

3. Document Path Schemas

/**
 * User state paths
 * @param {number} id - User ID
 * @returns {string} State path for user data
 * @example PATHS.userById(123) → "users.123"
 */
export const PATHS = {
    userById: (id) => `users.${id}`,

    /**
     * API endpoint state paths
     * @param {string} endpoint - API endpoint name
     * @param {'loading'|'error'|'data'} property - State property
     * @returns {string} State path for API state
     */
    apiState: (endpoint, property) => `api.${endpoint}.${property}`
};
Enter fullscreen mode Exit fullscreen mode

Anti-Patterns to Avoid

1. Over-Engineering Simple Cases

// ❌ Overkill for simple static paths
const PATHS = {
    getAuthToken: () => 'auth.token'  // Just use 'auth.token' directly
};

// ✅ Use constants for static paths
const AUTH_TOKEN = 'auth.token';
Enter fullscreen mode Exit fullscreen mode

2. Complex Logic in Path Builders

// ❌ Too much logic in path builders
const PATHS = {
    userPath: (id, includeProfile = false, includeSettings = false) => {
        let path = `users.${id}`;
        if (includeProfile) path += '.profile';
        if (includeSettings) path += '.settings';
        return path;
    }
};

// ✅ Keep path builders simple
const PATHS = {
    userById: (id) => `users.${id}`,
    userProfile: (id) => `users.${id}.profile`,
    userSettings: (id) => `users.${id}.settings`
};
Enter fullscreen mode Exit fullscreen mode

3. Inconsistent Parameter Order

// ❌ Inconsistent parameter order
const PATHS = {
    formField: (form, field) => `forms.${form}.${field}`,
    apiResource: (property, endpoint) => `api.${endpoint}.${property}`  // Different order
};

// ✅ Consistent parameter order
const PATHS = {
    formField: (form, field) => `forms.${form}.${field}`,
    apiResource: (endpoint, property) => `api.${endpoint}.${property}`
};
Enter fullscreen mode Exit fullscreen mode

Enterprise-Level State Organization

Module-Based Path Organization

// modules/auth/paths.js
export const AUTH_PATHS = {
    token: 'auth.token',
    user: 'auth.user',
    sessionById: (id) => `auth.sessions.${id}`
};

// modules/user/paths.js
export const USER_PATHS = {
    byId: (id) => `users.${id}`,
    listByRole: (role) => `users.roles.${role}`,
    activity: (userId, type) => `users.${userId}.activity.${type}`
};

// Central index
export { AUTH_PATHS } from './modules/auth/paths.js';
export { USER_PATHS } from './modules/user/paths.js';
Enter fullscreen mode Exit fullscreen mode

Environment-Specific Paths

const createPaths = (env) => ({
    cache: (key) => `${env}.cache.${key}`,
    temp: (session) => `${env}.temp.${session}`,
    logs: (level) => `${env}.logs.${level}`
});

export const DEV_PATHS = createPaths('dev');
export const PROD_PATHS = createPaths('prod');
export const PATHS = process.env.NODE_ENV === 'production' ? PROD_PATHS : DEV_PATHS;
Enter fullscreen mode Exit fullscreen mode

Migration Strategy

Gradual Migration from Strings

// Phase 1: Introduce constants for new features
const NEW_PATHS = {
    USER_PROFILE: 'user.profile',
    featureFlag: (flag) => `features.${flag}`
};

// Phase 2: Migrate existing critical paths
const LEGACY_PATHS = {
    AUTH_TOKEN: 'auth.token',  // Was: 'auth.token'
    USER_DATA: 'user.data'     // Was: 'user.data'
};

// Phase 3: Combine into unified system
export const PATHS = {
    ...LEGACY_PATHS,
    ...NEW_PATHS
};
Enter fullscreen mode Exit fullscreen mode

Conclusion

Dynamic path builders represent a significant evolution in state management for large Juris applications. They provide:

  • Type safety through validation
  • Maintainability through centralized path management
  • Flexibility through parameterized generation
  • Discoverability through organized path schemas
  • Performance through optimized path creation

By adopting dynamic path builders, teams can build more robust, maintainable, and scalable Juris applications while preserving the framework's simplicity and flexibility.

The pattern scales from simple constants for small apps to sophisticated path generation systems for enterprise applications, making it a versatile solution for teams of any size.

Top comments (0)