Kliensoldali webprogramozás
Horváth Győző
Egyetemi docens
1117 Budapest, Pázmány Péter sétány 1/c., 2.408-as szoba
Tel: (1) 372-2500/8469
horvath.gyozo@inf.elte.hu
useState
(state, action) → newState
State
{ /* ... */ }
Action
{ type: 'ADD_TODO', text: 'Go to swimming pool' }
Action creator
const addTodo = text => ({
type: ADD_TODO,
text
})
Reducer
function reducer(state = initialState, action) {
// Create the new state
return state
}
Store
const store = createStore(reducer)
const middleware1 = store => next => action => {
return next(action)
}
import { createStore, applyMiddleware } from 'redux'
const store = createStore(
rootReducer,
applyMiddleware(middleware1, middleware2)
)
// middlewares
const middleware1 = store => next => action => {
console.log('middleware1', action, next)
const result = next(action)
console.log('middleware1', result)
return 'alma'
}
const middleware2 = store => next => action => {
// store.dispatch(makeTip('z'))
console.log('middleware2', action, next)
const result = next(action)
console.log('middleware2', result)
return 'korte'
}
// store
const store = createStore(rootReducer, getInitialState(),
applyMiddleware(middleware1, middleware2))
const logger = store => next => action => {
console.log('dispatching', action)
let result = next(action)
console.log('next state', store.getState())
return result
}
const confirmationMiddleware = store => next => action => {
if (action.shouldConfirm) {
if (confirm('Are you sure?')) {
next(action)
}
} else {
next(action)
}
}
const timeoutScheduler = store => next => action => {
if (!action.meta || !action.meta.delay) {
return next(action)
}
const timeoutId = setTimeout(() => next(action), action.meta.delay)
return function cancel() {
clearTimeout(timeoutId)
}
}
const vanillaPromise = store => next => action => {
if (typeof action.then !== 'function') {
return next(action)
}
return Promise.resolve(action).then(store.dispatch)
}
const thunk = store => next => action =>
typeof action === 'function'
? action(store.dispatch, store.getState)
: next(action)
fetch
Callback
setTimeout(() => {
// run after 1s
}, 1000)
const xhr = new XMLHttpRequest();
xhr.addEventListener('load', function () {
// run after response is received
});
xhr.open('GET', `http://www.omdbapi.com/?t=${title}&apikey=<key>`);
xhr.send(null);
Promise, Promise.all()
, Promise.race()
,
Promise.resolve()
const promise = new Promise((resolve, reject) => {
resolve(value)
})
promise.then(value => {
// do something with value
})
fetch(`http://www.omdbapi.com/?t=${title}&apikey=2dd0dbee`)
.then(response => response.json())
.then(dataAsJson => {
// do something with dataAsJson
})
async
-await
függvények
async function () {
const value = await anAsyncApi()
return value
}
async function getPoster() {
const title = document.querySelector('input').value
const response = await fetch(`http://www.omdbapi.com/?t=${title}&apikey=2dd0dbee`)
const dataAsJson = await response.json()
// do something with dataAsJson
}
const delay = ms => new Promise((resolve, reject) => setTimeout(() => resolve(), ms))
class StorageApi {
constructor(items = [], ms = 0) {
this.ms = ms
this.items = items
}
async getItems() {
await delay(this.ms)
return this.items
}
async addItem(item) {
await delay(this.ms)
const id = this.items.length
item.id = id
this.items.push(item)
return item
}
}
const api = new StorageApi([{id: 0, fruit: 'apple'}, {id: 1, fruit: 'pear'}], 1000)
function App() {
const [items, setItems] = useState([])
useEffect(() => { // 😀
const getAll = async () => setItems(await api.getItems())
getAll()
}, [])
return (
<ul>
{items.map(({id, fruit}) =>
<li key={id}>{fruit}</li>
)}
</ul>
)
}
function App() {
const [items, setItems] = useState([])
api.getItems().then(items => setItems(items)) // 😕 fetches on every rerender, cannot rerender on prop change
return (
<ul>
{items.map(({id, fruit}) =>
<li key={id}>{fruit}</li>
)}
</ul>
)
}
function App() {
const [items, setItems] = useState([])
const [value, setValue] = useState('plum')
useEffect(() => {
const getAll = async () => setItems([...await api.getItems()])
getAll()
}, [])
const handleSubmit = async e => {
e.preventDefault()
const newItem = await api.addItem({fruit: value})
setItems(items.concat(newItem))
}
return (
<div style={{backgroundColor: randomColor(), padding: '10px'}}>
<form onSubmit={handleSubmit}>
<input type="text" value={value} onChange={e => setValue(e.target.value)} />
</form>
<ul>
{items.map(({id, fruit}) =>
<li key={id}>{fruit}</li>
)}
</ul>
</div>
)
}
const useItemsService = () => {
const [items, setItems] = useState([])
useEffect(() => {
const getAll = async () => setItems([...await api.getItems()])
getAll()
}, [])
const addNewItem = async item => {
const newItem = await api.addItem(item)
setItems(items => items.concat(newItem))
}
return { items, addNewItem }
}
const ItemsContext = React.createContext()
const ItemsProvider = ({children}) => {
const itemsService = useItemsService()
return <ItemsContext.Provider value={itemsService}>{children}</ItemsContext.Provider>
}
function App() {
const {items, addNewItem} = useContext(ItemsContext)
const [value, setValue] = useState('plum')
const handleSubmit = async e => {
e.preventDefault()
await addNewItem({fruit: value})
}
return (
<div style={{backgroundColor: randomColor(), padding: '10px'}}>
<form onSubmit={handleSubmit}>
<input type="text" value={value} onChange={e => setValue(e.target.value)} />
</form>
<ul>
{items.map(({id, fruit}) =>
<li key={id}>{fruit}</li>
)}
</ul>
</div>
)
}
ReactDOM.render(
<ItemsProvider>
<App />
</ItemsProvider>,
document.getElementById('root')
)
Store
// State
const initialState = {
isFetching: false,
items: [],
}
// Action types
const FETCH_ITEMS_REQUEST = 'FETCH_ITEMS_REQUEST'
const FETCH_ITEMS_SUCCESS = 'FETCH_ITEMS_SUCCESS'
const SAVE_ITEM_REQUEST = 'SAVE_ITEM_REQUEST'
const SAVE_ITEM_SUCCESS = 'SAVE_ITEM_SUCCESS'
// Action creators
const fetchItemsRequest = () => ({
type: FETCH_ITEMS_REQUEST
})
const fetchItemsSuccess = (items) => ({
type: FETCH_ITEMS_SUCCESS,
items
})
const saveItemRequest = () => ({
type: SAVE_ITEM_REQUEST
})
const saveItemSuccess = (item) => ({
type: SAVE_ITEM_SUCCESS,
item
})
// Reducer
const rootReducer = (state = initialState, action) => {
switch (action.type) {
case FETCH_ITEMS_REQUEST:
case SAVE_ITEM_REQUEST:
return {...state, isFetching: true}
case FETCH_ITEMS_SUCCESS:
return {...state, isFetching: false, items: [...action.items]}
case SAVE_ITEM_SUCCESS:
return {...state, isFetching: false, items: state.items.concat(action.item)}
default:
return state
}
}
// Store + middlewares
const loggerMiddleware = createLogger()
const store = createStore(
rootReducer,
applyMiddleware(
thunkMiddleware, // lets us dispatch() functions
loggerMiddleware // neat middleware that logs actions
)
)
// Synchronous test
store.dispatch(fetchItemsRequest())
store.dispatch(fetchItemsSuccess([{id: 0, fruit: 'apple'}, {id: 1, fruit: 'pear'}]))
store.dispatch(saveItemRequest({fruit: 'plum'}))
store.dispatch(saveItemSuccess({fruit: 'plum', id: 2}))
Szinkron hívás, aszinkronitás a komponensben
function App() {
const items = useSelector(state => state.items)
const dispatch = useDispatch()
const [value, setValue] = useState('plum')
useEffect(() => {
const getAll = async () => {
dispatch(fetchItemsRequest())
const items = await api.getItems()
dispatch(fetchItemsSuccess(items))
}
getAll()
}, [dispatch])
const handleSubmit = async e => {
e.preventDefault()
dispatch(saveItemRequest())
const item = await api.addItem({fruit: e.target.value})
dispatch(saveItemSuccess(item))
}
return (
<div style={{backgroundColor: randomColor(), padding: '10px'}}>
<form onSubmit={handleSubmit}>
<input type="text" value={value} onChange={e => setValue(e.target.value)} />
</form>
<ul>
{items.map(({id, fruit}) =>
<li key={id}>{fruit}</li>
)}
</ul>
</div>
)
}
Szinkron hívás, külön függvények
const getAll = async dispatch => {
dispatch(fetchItemsRequest())
const items = await api.getItems()
dispatch(fetchItemsSuccess(items))
}
const save = async (dispatch, value) => {
dispatch(saveItemRequest())
const item = await api.addItem({fruit: value})
dispatch(saveItemSuccess(item))
}
function App() {
const items = useSelector(state => state.items)
const dispatch = useDispatch()
const [value, setValue] = useState('plum')
useEffect(() => {
getAll(dispatch)
}, [dispatch])
const handleSubmit = e => {
e.preventDefault()
save(dispatch, e.target.value)
}
return (
<div style={{backgroundColor: randomColor(), padding: '10px'}}>
<form onSubmit={handleSubmit}>
<input type="text" value={value} onChange={e => setValue(e.target.value)} />
</form>
<ul>
{items.map(({id, fruit}) =>
<li key={id}>{fruit}</li>
)}
</ul>
</div>
)
}
Thunk middleware, aszinkron akciók
// Async action creators
const getAll = () => async dispatch => {
dispatch(fetchItemsRequest())
const items = await api.getItems()
dispatch(fetchItemsSuccess(items))
}
const save = value => async dispatch => {
dispatch(saveItemRequest())
const item = await api.addItem({fruit: value})
dispatch(saveItemSuccess(item))
}
function App() {
const items = useSelector(state => state.items)
const isFetching = useSelector(state => state.isFetching)
const dispatch = useDispatch()
const [value, setValue] = useState('plum')
useEffect(() => {
dispatch(getAll())
}, [dispatch])
const handleSubmit = e => {
e.preventDefault()
dispatch(save(value))
}
return (
<div style={{backgroundColor: randomColor(), padding: '10px', border: `${isFetching ? 5 : 0}px solid red`}}>
<form onSubmit={handleSubmit}>
<input type="text" value={value} onChange={e => setValue(e.target.value)} />
</form>
<ul>
{items.map(({id, fruit}) =>
<li key={id}>{fruit}</li>
)}
</ul>
</div>
)
}
Függvény akciók
npm install --save redux-thunk
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import rootReducer from './reducers/index';
const store = createStore(rootReducer, applyMiddleware(thunk));
function createThunkMiddleware(extraArgument) {
return ({ dispatch, getState }) => (next) => (action) => {
if (typeof action === 'function') {
return action(dispatch, getState, extraArgument);
}
return next(action);
};
}
// szinkron
const fetchItemsRequest = () => ({
type: FETCH_ITEMS_REQUEST
})
// aszinkron
const getAll = () => async dispatch => {
dispatch(fetchItemsRequest())
const items = await api.getItems()
dispatch(fetchItemsSuccess(items))
}
// aszinkron
const getAll = () => dispatch => {
dispatch(fetchItemsRequest())
api.getItems().then(items => dispatch(fetchItemsSuccess(items)))
}
const INCREMENT_COUNTER = 'INCREMENT_COUNTER';
function increment() {
return {
type: INCREMENT_COUNTER,
};
}
function incrementAsync() {
return (dispatch) => {
setTimeout(() => {
dispatch(increment());
}, 1000);
};
}
function fetchPosts(subreddit) {
return dispatch => {
dispatch(requestPosts(subreddit))
return fetch(`https://www.reddit.com/r/${subreddit}.json`)
.then(response => response.json())
.then(json => dispatch(receivePosts(subreddit, json)))
}
}
function shouldFetchPosts(state, subreddit) {
return true
}
export function fetchPostsIfNeeded(subreddit) {
return (dispatch, getState) => {
if (shouldFetchPosts(getState(), subreddit)) {
// Dispatch a thunk from thunk!
return dispatch(fetchPosts(subreddit))
} else {
// Let the calling code know there's nothing to wait for.
return Promise.resolve()
}
}
}
Egymásba ágyazott információk
const blogPosts = [
{
id: 'post1',
author: { username: 'user1', name: 'User 1' },
body: '......',
comments: [
{
id: 'comment1',
author: { username: 'user2', name: 'User 2' },
comment: '.....'
},
{
id: 'comment2',
author: { username: 'user3', name: 'User 3' },
comment: '.....'
}
]
},
{
id: 'post2',
author: { username: 'user2', name: 'User 2' },
body: '......',
comments: [
{
id: 'comment3',
author: { username: 'user3', name: 'User 3' },
comment: '.....'
},
{
id: 'comment4',
author: { username: 'user1', name: 'User 1' },
comment: '.....'
},
{
id: 'comment5',
author: { username: 'user3', name: 'User 3' },
comment: '.....'
}
]
}
// and repeat many times
]
Normalizált struktúra, “táblázatok”
{
posts : {
byId : {
"post1" : {
id : "post1",
author : "user1",
body : "......",
comments : ["comment1", "comment2"]
},
"post2" : {
id : "post2",
author : "user2",
body : "......",
comments : ["comment3", "comment4", "comment5"]
}
},
allIds : ["post1", "post2"]
},
comments : {
byId : {
"comment1" : {
id : "comment1",
author : "user2",
comment : ".....",
},
"comment2" : {
id : "comment2",
author : "user3",
comment : ".....",
},
"comment3" : {
id : "comment3",
author : "user3",
comment : ".....",
},
"comment4" : {
id : "comment4",
author : "user1",
comment : ".....",
},
"comment5" : {
id : "comment5",
author : "user3",
comment : ".....",
},
},
allIds : ["comment1", "comment2", "comment3", "commment4", "comment5"]
},
users : {
byId : {
"user1" : {
username : "user1",
name : "User 1",
},
"user2" : {
username : "user2",
name : "User 2",
},
"user3" : {
username : "user3",
name : "User 3",
}
},
allIds : ["user1", "user2", "user3"]
}
}
{
simpleDomainData1: {....},
simpleDomainData2: {....},
entities : {
entityType1 : {....},
entityType2 : {....}
},
ui : {
uiSection1 : {....},
uiSection2 : {....}
}
}
const { Map } = require('immutable');
const map1 = Map({ a: 1, b: 2, c: 3 });
const map2 = map1.set('b', 50);
map1.get('b') + " vs. " + map2.get('b'); // 2 vs. 50
import produce from "immer"
const baseState = [{
todo: "Learn typescript",
done: true
}, {
todo: "Try immer",
done: false
}]
const nextState = produce(baseState, draftState => {
draftState.push({todo: "Tweet about it"})
draftState[1].done = true
})
// First, define the reducer and action creators via `createSlice`
const usersSlice = createSlice({
name: 'users',
initialState: {
loading: 'idle',
users: [],
},
reducers: {
usersLoading(state, action) {
// Use a "state machine" approach for loading state instead of booleans
if (state.loading === 'idle') {
state.loading = 'pending'
}
},
usersReceived(state, action) {
if (state.loading === 'pending') {
state.loading = 'idle'
state.users = action.payload
}
},
},
})
// Destructure and export the plain action creators
export const { usersLoading, usersReceived } = usersSlice.actions
// Define a thunk that dispatches those action creators
const fetchUsers = () => async (dispatch) => {
dispatch(usersLoading())
const response = await usersAPI.fetchAll()
dispatch(usersReceived(response.data))
}
const getRepoDetailsStarted = () => ({
type: "repoDetails/fetchStarted"
})
const getRepoDetailsSuccess = (repoDetails) => ({
type: "repoDetails/fetchSucceeded",
payload: repoDetails
})
const getRepoDetailsFailed = (error) => ({
type: "repoDetails/fetchFailed",
error
})
const fetchIssuesCount = (org, repo) => async dispatch => {
dispatch(getRepoDetailsStarted())
try {
const repoDetails = await getRepoDetails(org, repo)
dispatch(getRepoDetailsSuccess(repoDetails))
} catch (err) {
dispatch(getRepoDetailsFailed(err.toString()))
}
}
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'
import { userAPI } from './userAPI'
// First, create the thunk
const fetchUserById = createAsyncThunk(
'users/fetchByIdStatus',
async (userId, thunkAPI) => {
const response = await userAPI.fetchById(userId)
return response.data
}
)
// Then, handle actions in your reducers:
const usersSlice = createSlice({
name: 'users',
initialState: { entities: [], loading: 'idle' },
reducers: {
// standard reducer logic, with auto-generated action types per reducer
},
extraReducers: (builder) => {
// Add reducers for additional action types here, and handle loading state as needed
builder.addCase(fetchUserById.fulfilled, (state, action) => {
// Add user to the state array
state.entities.push(action.payload)
})
},
})
// Later, dispatch the thunk as needed in the app
dispatch(fetchUserById(123))
createEntityAdapter
import {
createEntityAdapter,
createSlice,
configureStore,
} from '@reduxjs/toolkit'
const booksAdapter = createEntityAdapter()
const booksSlice = createSlice({
name: 'books',
initialState: booksAdapter.getInitialState(),
reducers: {
bookAdded: booksAdapter.addOne,
booksReceived(state, action) {
booksAdapter.setAll(state, action.payload.books)
},
},
})
const store = configureStore({
reducer: {
books: booksSlice.reducer,
},
})
console.log(store.getState().books)
// { ids: [], entities: {} }
// Can create a set of memoized selectors based on the location of this entity state
const booksSelectors = booksAdapter.getSelectors((state) => state.books)
// And then use the selectors to retrieve values
const allBooks = booksSelectors.selectAll(store.getState())
import {
createSlice,
createAsyncThunk,
createEntityAdapter,
} from '@reduxjs/toolkit'
import userAPI from './userAPI'
export const fetchUsers = createAsyncThunk('users/fetchAll', async () => {
const response = await userAPI.fetchAll()
// In this case, `response.data` would be:
// [{id: 1, first_name: 'Example', last_name: 'User'}]
return response.data
})
export const updateUser = createAsyncThunk('users/updateOne', async (arg) => {
const response = await userAPI.updateUser(arg)
// In this case, `response.data` would be:
// { id: 1, first_name: 'Example', last_name: 'UpdatedLastName'}
return response.data
})
export const usersAdapter = createEntityAdapter()
// By default, `createEntityAdapter` gives you `{ ids: [], entities: {} }`.
// If you want to track 'loading' or other keys, you would initialize them here:
// `getInitialState({ loading: false, activeRequestId: null })`
const initialState = usersAdapter.getInitialState()
export const slice = createSlice({
name: 'users',
initialState,
reducers: {
removeUser: usersAdapter.removeOne,
},
extraReducers: (builder) => {
builder.addCase(fetchUsers.fulfilled, usersAdapter.upsertMany)
builder.addCase(updateUser.fulfilled, (state, { payload }) => {
const { id, ...changes } = payload
usersAdapter.updateOne(state, { id, changes })
})
},
})
const reducer = slice.reducer
export default reducer
export const { removeUser } = slice.actions
const globalState = {
projects,
teams,
tasks,
users,
themeMode,
sidebarStatus,
}
=>
const serverState = {
projects,
teams,
tasks,
users,
}
const globalState = {
themeMode,
sidebarStatus,
}
Definíció
// Need to use the React-specific entry point to import createApi
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
// Define a service using a base URL and expected endpoints
export const pokemonApi = createApi({
reducerPath: 'pokemonApi',
baseQuery: fetchBaseQuery({ baseUrl: 'https://pokeapi.co/api/v2/' }),
endpoints: (builder) => ({
getPokemonByName: builder.query({
query: (name) => `pokemon/${name}`,
}),
}),
})
// Export hooks for usage in functional components, which are
// auto-generated based on the defined endpoints
export const { useGetPokemonByNameQuery } = pokemonApi
Regisztráció
import { configureStore } from '@reduxjs/toolkit'
// Or from '@reduxjs/toolkit/query/react'
import { setupListeners } from '@reduxjs/toolkit/query'
import { pokemonApi } from './services/pokemon'
export const store = configureStore({
reducer: {
// Add the generated reducer as a specific top-level slice
[pokemonApi.reducerPath]: pokemonApi.reducer,
},
// Adding the api middleware enables caching, invalidation, polling,
// and other useful features of `rtk-query`.
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(pokemonApi.middleware),
})
// optional, but required for refetchOnFocus/refetchOnReconnect behaviors
// see `setupListeners` docs - takes an optional callback as the 2nd arg for customization
setupListeners(store.dispatch)
Használat
import * as React from 'react'
import { useGetPokemonByNameQuery } from './services/pokemon'
export default function App() {
// Using a query hook automatically fetches data and returns query values
const { data, error, isLoading } = useGetPokemonByNameQuery('bulbasaur')
// Individual hooks are also accessible under the generated endpoints:
// const { data, error, isLoading } = pokemonApi.endpoints.getPokemonByName.useQuery('bulbasaur')
return (
<div className="App">
{error ? (
<>Oh no, there was an error</>
) : isLoading ? (
<>Loading...</>
) : data ? (
<>
<h3>{data.species.name}</h3>
<img src={data.sprites.front_shiny} alt={data.species.name} />
</>
) : null}
</div>
)
}
import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react";
const BASE_URL = "http://localhost:3030/";
const apiSlice = createApi({
reducerPath: "playlistsApi",
baseQuery: fetchBaseQuery({
baseUrl: BASE_URL,
}),
tagTypes: ["Playlist", "Track"],
endpoints: (build) => ({
getPlaylists: build.query({
query: (id) => ({ url: "playlists" }),
transformResponse: (response) => response.data,
providesTags: () => ["Playlist"],
}),
postPlaylist: build.mutation({
query: (body) => ({
url: "playlists",
method: "POST",
body: body,
}),
transformResponse: (response) => response.data,
invalidatesTags: ["Playlist"],
}),
putPlaylist: build.mutation({
query: ({ id, ...body }) => ({
url: `playlists/${id}`,
method: "PUT",
body: body,
}),
transformResponse: (response) => response.data,
invalidatesTags: ["Playlist"],
}),
getTracks: build.query({
query: (id) => ({ url: "tracks" }),
transformResponse: (response) => response.data,
providesTags: () => ["Track"],
}),
postTrack: build.mutation({
query: (body) => ({
url: "tracks",
method: "POST",
body: body,
}),
transformResponse: (response) => response.data,
invalidatesTags: ["Track"],
}),
putTrack: build.mutation({
query: ({ id, ...body }) => ({
url: `tracks/${id}`,
method: "PUT",
body: body,
}),
transformResponse: (response) => response.data,
invalidatesTags: ["Playlist", "Track"],
}),
deleteTrack: build.mutation({
query: (id) => ({
url: `tracks/${id}`,
method: "DELETE",
}),
transformResponse: (response) => response.data,
invalidatesTags: ["Playlist", "Track"],
}),
}),
});
export const {
useGetPlaylistsQuery,
usePostPlaylistMutation,
usePutPlaylistMutation,
useGetTracksQuery,
usePostTrackMutation,
usePutTrackMutation,
useDeleteTrackMutation,
} = apiSlice;
Használat
const Tracks = () => {
const { data: tracks = [] } = useGetTracksQuery();
const [postTrack] = usePostTrackMutation();
const [putTrack] = usePutTrackMutation();
const [deleteTrack] = useDeleteTrackMutation();
const handleSubmit = (values) => {
if (editedTrack) {
putTrack({ ...editedTrack, ...values });
} else {
postTrack(values);
}
};
return (/* ... */);
};
László Tamás diplomamunkája alapján
import { observable, action } from "mobx";
import { observer } from "mobx-react-lite";
import CounterProvider, { CounterContext } from "./CounterContext";
class CounterStore {
@observable counter = 0;
@action.bound increaseCounter() {
this.counter += 1;
}
}
const counterStore = new CounterStore();
export const CounterContext = React.createContext();
export default function CounterProvider({ children }) {
return (
<CounterContext.Provider value={counterStore}>
{children}
</CounterContext.Provider>
);
}
function Button() {
const { counter, increaseCounter } = React.useContext(CounterContext);
return <button onClick={increaseCounter}>Clicked {counter} times</button>;
}
const ObservedButton = observer(Button);
const Application = (
<CounterProvider>
<ObservedButton />
</CounterProvider>
);
const Store = types
.model("Store", {
counter: 0
})
.actions(self => ({
increaseCounter() {
self.counter += 1;
}
}));
const store = Store.create({});
import { Machine } from 'xstate';
const lightMachine = Machine({
id: 'light',
initial: 'green',
states: {
green: {
on: {
TIMER: 'yellow'
}
},
yellow: {
on: {
TIMER: 'red'
}
},
red: {
on: {
TIMER: 'green'
}
}
}
});
const currentState = 'green';
const nextState = lightMachine.transition(currentState, 'TIMER').value;
// => 'yellow'