Skip to content

Commit

Permalink
GH-107: Redux Management System
Browse files Browse the repository at this point in the history
Authored-by: navigsb <navigsb@uw.edu>
  • Loading branch information
NavigSB committed Aug 4, 2021
1 parent 6119f82 commit 79e90d5
Show file tree
Hide file tree
Showing 7 changed files with 377 additions and 24 deletions.
12 changes: 8 additions & 4 deletions frontend/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,17 @@ import { StatusBar } from "expo-status-bar";
import React from "react";
import { StyleSheet, Text, View } from "react-native";
import { registerRootComponent } from "expo";
import { Provider } from "react-redux";
import store from "./store/store";

function App() {
return (
<View style={styles.container}>
<Text>Open up App.js to start working on your app!</Text>
<StatusBar style="auto" />
</View>
<Provider store={store}>
<View style={styles.container}>
<Text>I am an app now powered by Redux!</Text>
<StatusBar style="auto" />
</View>
</Provider>
);
}

Expand Down
57 changes: 57 additions & 0 deletions frontend/components/CounterExample/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/**
* This is an example component that shows how to use the sliceManager hooks to access Redux
* slices. The example slice used here is defined in "counterSlice.js".
*/
import React, { useState } from "react";
import { View, Button, Text, TextInput } from "react-native";
import { useReduxSlice, useReduxSliceProperty } from "../../store/sliceManager";
import counterSlice from "../../store/slices/counterSlice";

function CounterExample() {
const [incrementAmount, setIncrementAmount] = useState("2");
// This is the hook that stores the slice methods, which can just be called directly.
const counterInterface = useReduxSlice(counterSlice);
// This is the hook that monitors a property, the "value" property in this case.
const count = useReduxSliceProperty(counterSlice, "value");

return (
<View style={{justifyContent: "space-around", height: 300}}>
<View style={{ flexDirection: "row", justifyContent: "space-evenly" }}>
<Button
onPress={() => counterInterface.increment()}
title="+"
/>
<Text style={{fontSize: 30}}>{count}</Text>
<Button
onPress={() => counterInterface.decrement()}
title="-"
/>
</View>
<TextInput
style={{fontSize: 30, width: 390, backgroundColor: "grey", color: "white", marginTop: 75}}
value={incrementAmount}
onChangeText={(text) => setIncrementAmount(text)}
/>
<View style={{ flexDirection: "row", justifyContent: "space-evenly" }}>
<Button
onPress={() => {
counterInterface.incrementByAmount(Number(incrementAmount) || 0);
}}
title="Add Amount"
/>
<Button
onPress={() =>
counterInterface.incrementAsync(Number(incrementAmount) || 0)
}
title="Add Async"
/>
<Button
onPress={() => counterInterface.setToRandomNumber()}
title="Set to Random Number"
/>
</View>
</View>
);
}

export default CounterExample;
131 changes: 131 additions & 0 deletions frontend/store/sliceManager.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import { createSlice } from "@reduxjs/toolkit";
import { useDispatch, useSelector } from "react-redux";

/**
* Creates a slice object (not equivalent to the Redux Toolkit slice) that the
* useReduxSlice hook below uses. The slice is created from a "layout", which is
* a function that returns an object with the properties: "name",
* "initialState", "functions", and "asyncFunctions".
*
* The "name" property is the name of the Toolkit slice to be created.
*
* The "initialState" property is the state initialized in the Toolkit slice upon
* creation.
*
* The "functions" property is an object that holds functions that change the
* state of the slice when called. Each function needs to have "state" as its first
* parameter. This "state" will be the state of the Toolkit slice in question.
* Using this parameter, you can directly modify the state. To increment a value
* in the state, for example, called "users", you would make a function called
* incrementUsers, with the "state" parameter, and call "state.users++" inside
* the function.
*
* The "asyncFunctions" property is similar to the functions property, except
* these functions do not directly modify the state of the slice. Instead,
* they are able to do asynchronous work and update the state via the functions
* in the functions property. To access those functions, the first parameter of
* all the functions in asyncFunctions has to be "functions". This will be
* equivalent to an exact replica of the "functions" property above, but do not
* dismiss the "functions" parameter, as they are not actually identical, and
* the difference is important.
*
* @param {Function} layout A function that returns an object with the
* properties: name, initialState, functions, and asyncFunctions.
* @returns A slice object as defined in this manager file.
*/
export const createSliceFromLayout = (layout) => {
let layoutObj = layout();

const reducers = {};
Object.keys(layoutObj.functions).forEach((key) => {
reducers[key] = (state, action) => {
layoutObj.functions[key](state, ...action.params);
};
});

const slice = createSlice({
name: layoutObj.name,
initialState: layoutObj.initialState,
reducers,
});
return {slice, layout};
};

/**
* A React hook which allows the client to access and call the methods in
* a slice.
* @param {object} sliceContainer The slice (as defined in this manager) object
* to access methods from.
* @returns An object that contains all the available functions.
*/
export const useReduxSlice = (sliceContainer) => {
let layout = sliceContainer.layout();
let slice = sliceContainer.slice;

const dispatch = useDispatch();

const funcs = slice.actions;
const asyncFuncs = layout.asyncFunctions;

const newFuncs = {};
Object.keys(funcs).forEach((key) => {
newFuncs[key] = (...params) => {
let result = funcs[key](...params);
dispatch({type: result.type, params: [...params]});
};
});

const newAsyncFuncs = {};
Object.keys(asyncFuncs).forEach((key) => {
newAsyncFuncs[key] = function(...params) {
dispatch(() => {
asyncFuncs[key](newFuncs, ...params);
})
}
});

return {
...newFuncs,
...newAsyncFuncs
};
};

/**
* A React hook which allows the user to monitor a part of the state of the
* given slice, at the given path. For each monitored property, this hook
* must be called again.
* @param {object} sliceContainer The slice (as defined in this manager) object
* to access the property from.
* @param {...String} path A set of Strings that define where to access the
* property in the state. For example, if I wanted to access
* state.users.john.likes, I would put after the "sliceContainer" parameter:
* "colors", "parimary", "blue".
* @returns The value of the specified property, which updates upon any changes.
*/
export const useReduxSliceProperty = (sliceContainer, ...path) => {
return useSelector((state) => {
let endVal = state[sliceContainer.slice.name];
let pathCopy = JSON.parse(JSON.stringify(path));
while(pathCopy.length > 0) {
endVal = endVal[pathCopy.shift()];
}
return endVal;
});
};

/**
* A helper function to remove the Redux jargon from store.js. This function is
* called only in store.js, in its configureStore method as the only parameter.
* It takes an object which contains key-value pairs of the name of the slice as
* the key, and the actual slice instance as the value.
* @param {object} slicesObj An object which contains key-value pairs, in this
* format: slice name: slice instance.
* @returns An object in the format that configureStore wants.
*/
export const getSlicesConfig = (slicesObj) => {
let reducers = {};
Object.keys(slicesObj).forEach((key) => {
reducers[key] = slicesObj[key].slice.reducer
});
return {reducer: reducers};
};
64 changes: 64 additions & 0 deletions frontend/store/slices/counterSlice.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/**
* This is an example slice to show how to create slice layout files for my Redux manager. It is
* the layout for the test component "CounterExample".
*/
import { createSliceFromLayout } from "../sliceManager";

const CounterLayout = () => {
const name = "counter";
const initialState = {
value: 0
};

// These are the functions that change the state when called. They need a state parameter to change
// things.
function increment(state) {
state.value += 1;
}

function decrement(state) {
state.value -= 1;
}

function incrementByAmount(state, amount) {
state.value += amount;
}

function setCount(state, amount) {
state.value = amount;
}

// These are the functions that help do asynchronous work. They only take the functions parameter,
// so they can manipulate the state throgh the synchronous functions above, but they cannot
// modify it directly. I just did this because it's what Redux tended to do in their examples.
async function incrementAsync(functions, amount) {
setTimeout(() => {
functions.incrementByAmount(amount);
}, 1000);
}

async function setToRandomNumber(functions) {
setTimeout(() => {
functions.setCount(Math.floor(Math.random() * 10));
}, 1000);
}

return {
name,
initialState,
functions: {
increment,
decrement,
incrementByAmount,
setCount
},
asyncFunctions: {
incrementAsync,
setToRandomNumber
}
};
}

// This createSliceFromLayout changes the layout defined above into something that Redux can actually
// use. For you to use it, you have to use the React hooks defined in sliceManager.js.
export default createSliceFromLayout(CounterLayout);
9 changes: 9 additions & 0 deletions frontend/store/store.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { configureStore } from "@reduxjs/toolkit";
import { getSlicesConfig } from "./sliceManager";
import counterSlice from "./slices/counterSlice";

export default configureStore(
getSlicesConfig({

})
);
Loading

0 comments on commit 79e90d5

Please sign in to comment.