We use cookies to enhance your experience on the site
CodeWorlds

Adapter and Facade Patterns

On our cosmic journey through React, our ship must connect with many different systems - space stations, satellites, planetary bases. Each of these systems has its own communication protocol. The Adapter and Facade patterns are like a universal translator that allows our ship to communicate with any system without needing to know its internal details.

Adapter Pattern

What is an Adapter?

The Adapter pattern (also known as Wrapper) allows objects with incompatible interfaces to work together. In React, we most commonly use it to wrap external APIs with a consistent, predictable interface.

Imagine that your cosmic weather database for different planets uses three different API providers. Each of them returns data in a different format. An Adapter allows you to unify this data:

1// Raw responses from different space APIs
2// Mars API returns: { temp_celsius: -60, wind_kph: 25, condition: "dusty" }
3// Jupiter API returns: { temperature: { value: -145, unit: "C" }, windSpeed: 350 }
4// Saturn API returns: { readings: { t: -178, w: 500 }, status: "stormy" }

Without an adapter, we would need to check in every component which API the data came from and process it accordingly. That leads to a mess!

Implementing an Adapter in React

Let's create adapters that unify data from different APIs:

1// Unified weather data interface
2// { planet, temperature, windSpeed, condition }
3
4function adaptMarsWeather(rawData) {
5  return {
6    planet: 'Mars',
7    temperature: rawData.temp_celsius,
8    windSpeed: rawData.wind_kph,
9    condition: rawData.condition,
10  };
11}
12
13function adaptJupiterWeather(rawData) {
14  return {
15    planet: 'Jupiter',
16    temperature: rawData.temperature.value,
17    windSpeed: rawData.windSpeed,
18    condition: rawData.windSpeed > 300 ? 'extreme' : 'moderate',
19  };
20}
21
22function adaptSaturnWeather(rawData) {
23  return {
24    planet: 'Saturn',
25    temperature: rawData.readings.t,
26    windSpeed: rawData.readings.w,
27    condition: rawData.status,
28  };
29}

Now we have three simple functions, each of which takes raw data and returns an object with an identical structure. Our weather component can use this data without knowing where it came from:

1function PlanetWeatherCard({ weatherData }) {
2  // weatherData ALWAYS has the same shape thanks to adapters
3  return (
4    <div className="weather-card">
5      <h3>{weatherData.planet}</h3>
6      <p>Temperature: {weatherData.temperature} C</p>
7      <p>Wind: {weatherData.windSpeed} km/h</p>
8      <p>Condition: {weatherData.condition}</p>
9    </div>
10  );
11}

Adapter as a Custom Hook

Adapters work great as custom hooks that wrap external libraries:

1// Adapter for different space map libraries
2function useSpaceMap(provider) {
3  const [map, setMap] = useState(null);
4
5  useEffect(() => {
6    let mapInstance;
7
8    if (provider === 'galactic-maps') {
9      // GalacticMaps library has its own API
10      mapInstance = new GalacticMaps.Chart('#map');
11      mapInstance.init({ zoom: 5 });
12    } else if (provider === 'star-atlas') {
13      // StarAtlas has a completely different API
14      mapInstance = StarAtlas.create(document.getElementById('map'), {
15        level: 5,
16        mode: '3d',
17      });
18    }
19
20    // We return a unified interface
21    setMap({
22      zoomIn: () => {
23        if (provider === 'galactic-maps') mapInstance.setZoom(mapInstance.getZoom() + 1);
24        else mapInstance.changeLevel(mapInstance.level + 1);
25      },
26      zoomOut: () => {
27        if (provider === 'galactic-maps') mapInstance.setZoom(mapInstance.getZoom() - 1);
28        else mapInstance.changeLevel(mapInstance.level - 1);
29      },
30      centerOn: (coords) => {
31        if (provider === 'galactic-maps') mapInstance.panTo(coords);
32        else mapInstance.focus({ x: coords.lat, y: coords.lng });
33      },
34    });
35
36    return () => {
37      if (provider === 'galactic-maps') mapInstance.destroy();
38      else mapInstance.dispose();
39    };
40  }, [provider]);
41
42  return map;
43}

Thanks to this hook, the component using the map doesn't need to know which library is being used underneath. If we wanted to change the map provider, we would only need to modify the hook - no component would change.

Facade Pattern

What is a Facade?

The Facade pattern provides a simplified interface to a complex subsystem. In React, we most commonly implement it as hooks that hide complicated logic behind a simple API.

Think of a spaceship's control panel. The pilot doesn't need to know the details of how each system works - they press the "Start engines" button and the entire complex process starts automatically. A Facade works exactly the same way.

Simple example: Hook as a Facade

Without a facade, a component has to manage many details itself:

1// WITHOUT Facade - the component is overwhelmed with details
2function MissionLauncher() {
3  const [fuel, setFuel] = useState(0);
4  const [systems, setSystems] = useState([]);
5  const [crewReady, setCrewReady] = useState(false);
6  const [countdown, setCountdown] = useState(null);
7  const [launchStatus, setLaunchStatus] = useState('idle');
8  const [errors, setErrors] = useState([]);
9
10  // Checking fuel
11  useEffect(() => {
12    fetchFuelLevel().then(setFuel);
13  }, []);
14
15  // Checking systems
16  useEffect(() => {
17    checkAllSystems().then(setSystems);
18  }, []);
19
20  // Countdown
21  useEffect(() => {
22    if (countdown !== null && countdown > 0) {
23      const timer = setTimeout(() => setCountdown(c => c - 1), 1000);
24      return () => clearTimeout(timer);
25    }
26    if (countdown === 0) {
27      executeLaunch().then(() => setLaunchStatus('launched'));
28    }
29  }, [countdown]);
30
31  const startLaunch = async () => {
32    if (fuel < 80) { setErrors(e => [...e, 'Not enough fuel']); return; }
33    if (!crewReady) { setErrors(e => [...e, 'Crew not ready']); return; }
34    const failedSystems = systems.filter(s => s.status !== 'ok');
35    if (failedSystems.length > 0) {
36      setErrors(e => [...e, 'Systems not operational']);
37      return;
38    }
39    setCountdown(10);
40  };
41
42  // ... rest of component with a huge amount of logic
43}

With a facade, we move all this logic into a hook:

1// WITH Facade - the hook hides all the complexity
2function useMissionLaunch() {
3  const [fuel, setFuel] = useState(0);
4  const [systems, setSystems] = useState([]);
5  const [crewReady, setCrewReady] = useState(false);
6  const [countdown, setCountdown] = useState(null);
7  const [status, setStatus] = useState('idle');
8  const [errors, setErrors] = useState([]);
9
10  useEffect(() => {
11    fetchFuelLevel().then(setFuel);
12    checkAllSystems().then(setSystems);
13  }, []);
14
15  useEffect(() => {
16    if (countdown !== null && countdown > 0) {
17      const timer = setTimeout(() => setCountdown(c => c - 1), 1000);
18      return () => clearTimeout(timer);
19    }
20    if (countdown === 0) {
21      executeLaunch().then(() => setStatus('launched'));
22    }
23  }, [countdown]);
24
25  const launch = () => {
26    const newErrors = [];
27    if (fuel < 80) newErrors.push('Not enough fuel');
28    if (!crewReady) newErrors.push('Crew not ready');
29    const failedSystems = systems.filter(s => s.status !== 'ok');
30    if (failedSystems.length > 0) newErrors.push('Systems not operational');
31
32    if (newErrors.length > 0) {
33      setErrors(newErrors);
34      return;
35    }
36    setCountdown(10);
37    setStatus('countdown');
38  };
39
40  const abort = () => {
41    setCountdown(null);
42    setStatus('aborted');
43  };
44
45  return {
46    // Simple, readable API
47    launch,
48    abort,
49    status,
50    countdown,
51    errors,
52    isReady: fuel >= 80 && crewReady && systems.every(s => s.status === 'ok'),
53    fuelLevel: fuel,
54    setCrewReady,
55  };
56}
57
58// The component is now clean and simple
59function MissionLauncher() {
60  const mission = useMissionLaunch();
61
62  return (
63    <div>
64      <h1>Mission Launch</h1>
65      <p>Fuel: {mission.fuelLevel}%</p>
66      <p>Status: {mission.status}</p>
67      {mission.countdown !== null && <p>Countdown: {mission.countdown}</p>}
68      {mission.errors.map((err, i) => <p key={i} style={{color: 'red'}}>{err}</p>)}
69      <button onClick={mission.launch} disabled={!mission.isReady}>
70        Launch
71      </button>
72      <button onClick={mission.abort}>Abort</button>
73    </div>
74  );
75}

Facade for complex operations

Facade is particularly useful when you need to coordinate multiple operations:

1function useCrewManagement(missionId) {
2  // Internally manages multiple data sources
3  const [crew, setCrew] = useState([]);
4  const [assignments, setAssignments] = useState({});
5  const [healthRecords, setHealthRecords] = useState({});
6
7  useEffect(() => {
8    Promise.all([
9      fetchCrew(missionId),
10      fetchAssignments(missionId),
11      fetchHealthRecords(missionId),
12    ]).then(([crewData, assignData, healthData]) => {
13      setCrew(crewData);
14      setAssignments(assignData);
15      setHealthRecords(healthData);
16    });
17  }, [missionId]);
18
19  // Simple interface for the component
20  return {
21    crewMembers: crew.map(member => ({
22      ...member,
23      role: assignments[member.id]?.role || 'Unassigned',
24      healthStatus: healthRecords[member.id]?.status || 'Unknown',
25    })),
26    assignRole: (memberId, role) => {
27      updateAssignment(missionId, memberId, role)
28        .then(() => setAssignments(prev => ({
29          ...prev,
30          [memberId]: { ...prev[memberId], role },
31        })));
32    },
33    isCrewReady: crew.length > 0 && crew.every(m =>
34      assignments[m.id]?.role && healthRecords[m.id]?.status === 'fit'
35    ),
36  };
37}

When to use Adapter vs Facade?

| Feature | Adapter | Facade | |---------|---------|--------| | Purpose | Unify incompatible interfaces | Simplify a complex system | | When | Multiple data sources with different formats | Complex logic to hide | | In React | Wrapping external APIs/libraries | Custom hooks hiding complexity | | Space analogy | Alien civilization language translator | Ship control panel |

Both patterns can be combined - a Facade can internally use adapters:

1function useGalacticWeather(planets) {
2  // Facade: simplifies the interface
3  const [weather, setWeather] = useState({});
4
5  useEffect(() => {
6    const adapters = {
7      mars: adaptMarsWeather,
8      jupiter: adaptJupiterWeather,
9      saturn: adaptSaturnWeather,
10    };
11
12    // Adapter: unifies data from different APIs
13    Promise.all(
14      planets.map(planet =>
15        fetchWeather(planet).then(raw => adapters[planet](raw))
16      )
17    ).then(results => {
18      const weatherMap = {};
19      results.forEach(w => { weatherMap[w.planet] = w; });
20      setWeather(weatherMap);
21    });
22  }, [planets]);
23
24  return {
25    getWeather: (planet) => weather[planet] || null,
26    isLoaded: Object.keys(weather).length === planets.length,
27    coldestPlanet: Object.values(weather)
28      .sort((a, b) => a.temperature - b.temperature)[0]?.planet || null,
29  };
30}

Summary

The Adapter and Facade patterns are essential tools in the arsenal of a cosmic React programmer:

  • Adapter allows you to unify incompatible interfaces, which is crucial when integrating with multiple external APIs and libraries
  • Facade simplifies complex subsystems, creating readable and easy-to-use hooks
  • Both patterns increase code readability, simplify testing, and reduce coupling between components

Just as a universal power adapter allows a spaceship to connect to any docking station in the galaxy, the Adapter and Facade patterns allow our React components to work with any system without unnecessary complications.

Go to CodeWorlds