react-native/Libraries/NavigationExperimental/Reducer/NavigationScenesReducer.js

211 lines
4.7 KiB
JavaScript

/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @providesModule NavigationScenesReducer
* @flow
*/
'use strict';
const invariant = require('fbjs/lib/invariant');
const shallowEqual = require('fbjs/lib/shallowEqual');
import type {
NavigationRoute,
NavigationScene,
NavigationState,
} from 'NavigationTypeDefinition';
const SCENE_KEY_PREFIX = 'scene_';
/**
* Helper function to compare route keys (e.g. "9", "11").
*/
function compareKey(one: string, two: string): number {
const delta = one.length - two.length;
if (delta > 0) {
return 1;
}
if (delta < 0) {
return -1;
}
return one > two ? 1 : -1;
}
/**
* Helper function to sort scenes based on their index and view key.
*/
function compareScenes(
one: NavigationScene,
two: NavigationScene,
): number {
if (one.index > two.index) {
return 1;
}
if (one.index < two.index) {
return -1;
}
return compareKey(
one.key,
two.key,
);
}
/**
* Whether two routes are the same.
*/
function areScenesShallowEqual(
one: NavigationScene,
two: NavigationScene,
): boolean {
return (
one.key === two.key &&
one.index === two.index &&
one.isStale === two.isStale &&
one.isActive === two.isActive &&
areRoutesShallowEqual(one.route, two.route)
);
}
/**
* Whether two routes are the same.
*/
function areRoutesShallowEqual(
one: ?NavigationRoute,
two: ?NavigationRoute,
): boolean {
if (!one || !two) {
return one === two;
}
if (one.key !== two.key) {
return false;
}
return shallowEqual(one, two);
}
function NavigationScenesReducer(
scenes: Array<NavigationScene>,
nextState: NavigationState,
prevState: ?NavigationState,
): Array<NavigationScene> {
if (prevState === nextState) {
return scenes;
}
const prevScenes: Map<string, NavigationScene> = new Map();
const freshScenes: Map<string, NavigationScene> = new Map();
const staleScenes: Map<string, NavigationScene> = new Map();
// Populate stale scenes from previous scenes marked as stale.
scenes.forEach(scene => {
const {key} = scene;
if (scene.isStale) {
staleScenes.set(key, scene);
}
prevScenes.set(key, scene);
});
const nextKeys = new Set();
nextState.routes.forEach((route, index) => {
const key = SCENE_KEY_PREFIX + route.key;
const scene = {
index,
isActive: false,
isStale: false,
key,
route,
};
invariant(
!nextKeys.has(key),
`navigationState.routes[${index}].key "${key}" conflicts with ` +
'another route!'
);
nextKeys.add(key);
if (staleScenes.has(key)) {
// A previously `stale` scene is now part of the nextState, so we
// revive it by removing it from the stale scene map.
staleScenes.delete(key);
}
freshScenes.set(key, scene);
});
if (prevState) {
// Look at the previous routes and classify any removed scenes as `stale`.
prevState.routes.forEach((route: NavigationRoute, index) => {
const key = SCENE_KEY_PREFIX + route.key;
if (freshScenes.has(key)) {
return;
}
staleScenes.set(key, {
index,
isActive: false,
isStale: true,
key,
route,
});
});
}
const nextScenes = [];
const mergeScene = (nextScene => {
const {key} = nextScene;
const prevScene = prevScenes.has(key) ? prevScenes.get(key) : null;
if (prevScene && areScenesShallowEqual(prevScene, nextScene)) {
// Reuse `prevScene` as `scene` so view can avoid unnecessary re-render.
// This assumes that the scene's navigation state is immutable.
nextScenes.push(prevScene);
} else {
nextScenes.push(nextScene);
}
});
staleScenes.forEach(mergeScene);
freshScenes.forEach(mergeScene);
nextScenes.sort(compareScenes);
let activeScenesCount = 0;
nextScenes.forEach((scene, ii) => {
const isActive = !scene.isStale && scene.index === nextState.index;
if (isActive !== scene.isActive) {
nextScenes[ii] = {
...scene,
isActive,
};
}
if (isActive) {
activeScenesCount++;
}
});
invariant(
activeScenesCount === 1,
'there should always be only one scene active, not %s.',
activeScenesCount,
);
if (nextScenes.length !== scenes.length) {
return nextScenes;
}
if (nextScenes.some(
(scene, index) => !areScenesShallowEqual(scenes[index], scene)
)) {
return nextScenes;
}
// scenes haven't changed.
return scenes;
}
module.exports = NavigationScenesReducer;