Skip to main content

Resolve

Resolves allow you to fetch data asynchronously before a state is activated. They ensure required data is available before the state’s views are rendered.

Resolve Basics

Define resolves in state declarations:
router.stateRegistry.register({
  name: 'user',
  url: '/users/:userId',
  resolve: {
    user: (UserService, $transition$) => {
      const userId = $transition$.params().userId;
      return UserService.getUser(userId);
    }
  }
});
The user resolve:
  • Executes before entering the state
  • Can inject services and dependencies
  • Returns data (or a promise for data)
  • Makes data available to views and child states

Resolve Syntax

Object Syntax

Simple key-value object:
resolve: {
  // Simple value
  appName: () => 'My Application',
  
  // Inject dependencies
  user: (UserService, $transition$) => {
    return UserService.getUser($transition$.params().userId);
  },
  
  // Depend on other resolves
  permissions: (user, PermissionService) => {
    return PermissionService.getForUser(user.id);
  },
  
  // Async with promises
  posts: async (user, PostService) => {
    return await PostService.getForUser(user.id);
  }
}

Array Syntax (Advanced)

Explicit dependency declaration:
resolve: [
  {
    token: 'user',
    deps: ['UserService', 'Transition'],
    resolveFn: (userService, transition) => {
      const userId = transition.params().userId;
      return userService.getUser(userId);
    },
    policy: {
      when: 'EAGER',
      async: 'WAIT'
    }
  },
  {
    token: 'posts',
    deps: ['user', 'PostService'],
    resolveFn: (user, postService) => {
      return postService.getForUser(user.id);
    }
  }
]

Injectable Dependencies

Available Tokens

Special injectable tokens:
resolve: {
  data: (
    $transition$,    // Current Transition object
    $state$,         // Current StateObject (for onEnter/onExit hooks)
    UIRouter,        // Router instance
    UserService      // Custom services
  ) => {
    // Use injected dependencies
  }
}

Transition Object

Access transition details:
resolve: {
  user: ($transition$) => {
    // Get parameters
    const userId = $transition$.params().userId;
    
    // Get states
    const fromState = $transition$.from();
    const toState = $transition$.to();
    
    // Get options
    const custom = $transition$.options().custom;
    
    return UserService.get(userId);
  }
}

Injecting Services

resolve: {
  // Service injection
  data: (HttpService, AuthService) => {
    const token = AuthService.getToken();
    return HttpService.get('/api/data', { token });
  }
}

Injecting Other Resolves

resolve: {
  // First resolve
  user: (UserService, $transition$) => {
    return UserService.get($transition$.params().userId);
  },
  
  // Second resolve depends on first
  profile: (user, ProfileService) => {
    return ProfileService.getForUser(user.id);
  },
  
  // Third depends on first and second
  settings: (user, profile, SettingsService) => {
    return SettingsService.get(user.id, profile.type);
  }
}

Resolve Lifecycle

When Resolves Execute

1
Transition starts
2
Transition object is created
3
EAGER resolves fetch
4
Resolves with when: 'EAGER' policy start fetching
5
State is about to be entered
6
Resolves with when: 'LAZY' policy (default) start fetching
7
All resolves complete
8
Wait for all resolves to finish (if async: 'WAIT')
9
State activates
10
Views render with resolved data available

Resolve Inheritance

Child states inherit parent resolves:
// Parent state
router.stateRegistry.register({
  name: 'app',
  resolve: {
    config: () => loadConfig(),
    user: (AuthService) => AuthService.getCurrentUser()
  }
});

// Child state
router.stateRegistry.register({
  name: 'app.dashboard',
  resolve: {
    // Can inject parent resolves
    stats: (user, StatsService) => {
      return StatsService.getForUser(user.id);
    }
  }
});

// Grandchild state
router.stateRegistry.register({
  name: 'app.dashboard.analytics',
  resolve: {
    // Can inject config, user, and stats
    report: (user, stats, ReportService) => {
      return ReportService.generate(user.id, stats);
    }
  }
});

Resolve Policies

Control when and how resolves are fetched:

When Policy

Controls when the resolve is fetched:
resolvePolicy: {
  when: 'LAZY'   // Fetch just before entering (default)
               // or 'EAGER' to fetch earlier in transition
}
LAZY (default):
  • Fetched just before entering the state
  • Only fetched if state will be entered
  • Good for most use cases
EAGER:
  • Fetched early in the transition
  • Fetched even if state might be redirected
  • Use for critical data needed for redirects

Async Policy

Controls if transition waits for resolve:
resolvePolicy: {
  async: 'WAIT'     // Wait for resolve (default)
               // or 'NOWAIT' to continue without waiting
}
WAIT (default):
  • Transition waits for resolve to complete
  • State doesn’t activate until data is ready
  • Views render with data available
NOWAIT:
  • Transition continues immediately
  • State activates without waiting
  • Views must handle loading state

State-Level Policy

Set default policy for all resolves in a state:
router.stateRegistry.register({
  name: 'dashboard',
  resolvePolicy: {
    when: 'EAGER',
    async: 'WAIT'
  },
  resolve: {
    data1: (Service1) => Service1.load(),
    data2: (Service2) => Service2.load()
    // Both use EAGER/WAIT policy
  }
});

Per-Resolve Policy

Override policy for individual resolves:
resolve: [
  {
    token: 'critical',
    deps: ['Service'],
    resolveFn: (svc) => svc.loadCritical(),
    policy: { when: 'EAGER', async: 'WAIT' }
  },
  {
    token: 'optional',
    deps: ['Service'],
    resolveFn: (svc) => svc.loadOptional(),
    policy: { when: 'LAZY', async: 'NOWAIT' }
  }
]

Accessing Resolved Data

In Views (Framework-Specific)

Resolved data is injected into views/components based on the framework.

In Transition Hooks

transitionService.onEnter({ entering: 'user' }, (transition) => {
  // Get all resolved data for 'user' state
  const injector = transition.injector();
  const user = injector.get('user');
  
  console.log('User:', user);
});

Async Access

transitionService.onBefore({ to: 'dashboard' }, async (transition) => {
  const injector = transition.injector();
  
  // Wait for resolve to complete
  const user = await injector.getAsync('user');
  
  console.log('User loaded:', user);
});

In Child Resolves

As shown earlier, child resolves can inject parent resolves as dependencies.

Error Handling

Resolve Errors

If a resolve rejects, the transition is aborted:
resolve: {
  user: (UserService, $transition$) => {
    const userId = $transition$.params().userId;
    return UserService.getUser(userId);
    // If this promise rejects, transition aborts
  }
}

Catching Errors

// In resolve function
resolve: {
  user: async (UserService, $transition$) => {
    try {
      const userId = $transition$.params().userId;
      return await UserService.getUser(userId);
    } catch (error) {
      console.error('Failed to load user:', error);
      // Return default value or rethrow
      return null;
    }
  }
}

// In transition hook
transitionService.onError({}, (transition) => {
  const error = transition.error();
  console.error('Transition failed:', error);
  
  // Redirect to error page
  return router.stateService.target('error', {
    message: error.message
  });
});

Common Patterns

Loading User Data

resolve: {
  currentUser: (AuthService) => {
    return AuthService.getCurrentUser();
  }
}

Loading Entity by ID

resolve: {
  article: (ArticleService, $transition$) => {
    const id = $transition$.params().articleId;
    return ArticleService.getById(id);
  }
}

Loading List Data

resolve: {
  products: (ProductService, $transition$) => {
    const params = $transition$.params();
    return ProductService.list({
      category: params.category,
      page: params.page,
      sort: params.sort
    });
  }
}

Dependent Resolves

resolve: {
  user: (UserService, $transition$) => {
    return UserService.get($transition$.params().userId);
  },
  
  posts: (user, PostService) => {
    return PostService.getForUser(user.id);
  },
  
  comments: (posts, CommentService) => {
    const postIds = posts.map(p => p.id);
    return CommentService.getForPosts(postIds);
  }
}

Parallel Loading

resolve: {
  // These load in parallel
  users: (UserService) => UserService.list(),
  products: (ProductService) => ProductService.list(),
  orders: (OrderService) => OrderService.list()
}

Conditional Loading

resolve: {
  userData: ($transition$, UserService, GuestService) => {
    const userId = $transition$.params().userId;
    
    if (userId === 'guest') {
      return GuestService.getGuestData();
    }
    
    return UserService.get(userId);
  }
}

Caching Resolves

let cachedConfig = null;

resolve: {
  config: async (ConfigService) => {
    if (cachedConfig) {
      return cachedConfig;
    }
    
    cachedConfig = await ConfigService.load();
    return cachedConfig;
  }
}

Pre-Resolved Data

Provide data without a resolve function:
resolve: [
  {
    token: 'staticData',
    resolveFn: null,
    data: { key: 'value' }  // Already resolved
  }
]

Dynamic Resolves

Add resolves programmatically:
transitionService.onCreate({}, (transition) => {
  // Add resolve during transition
  const resolves = transition.injector().get('$resolve$');
  
  // Add custom resolvable
  const customResolvable = new Resolvable(
    'dynamicData',
    () => loadDynamicData(),
    []
  );
  
  resolves.push(customResolvable);
});

Best Practices

1
Keep resolves focused
2
// Good - single responsibility
resolve: {
  user: (UserService) => UserService.current(),
  permissions: (PermissionService) => PermissionService.load()
}

// Avoid - doing too much
resolve: {
  everything: (Services) => loadEverything()
}
3
Use appropriate policies
4
// EAGER for critical data
resolvePolicy: { when: 'EAGER' }

// LAZY for most data (default)
resolvePolicy: { when: 'LAZY' }
5
Handle errors gracefully
6
resolve: {
  user: async (UserService) => {
    try {
      return await UserService.load();
    } catch (error) {
      console.error(error);
      return null;  // Provide fallback
    }
  }
}
7
Leverage resolve dependencies
8
// Good - declarative dependencies
resolve: {
  user: (UserService) => UserService.current(),
  profile: (user, ProfileService) => ProfileService.get(user.id)
}

// Avoid - nested promises
resolve: {
  data: async (UserService, ProfileService) => {
    const user = await UserService.current();
    const profile = await ProfileService.get(user.id);
    return { user, profile };
  }
}
9
Use parent resolves in abstract states
10
// Abstract parent
{
  name: 'app',
  abstract: true,
  resolve: {
    config: () => loadAppConfig()
  }
}

// Children inherit config
{
  name: 'app.home',
  resolve: {
    data: (config, DataService) => DataService.load(config)
  }
}

API Reference

Build docs developers (and LLMs) love