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
ui = f(state)
Data down, action up
let counter = 0
function increaseCounter(step = 1) {
counter += step
}
function decreaseCounter(step = 1) {
counter -= step
}
class Counter {
constructor() {
this.counter = 0
}
increaseCounter(step = 1) {
this.counter += step
}
decreaseCounter(step = 1) {
this.counter -= step
}
}
function Counter() {
const [counter, setCounter] = useState(0)
function increaseCounter(step = 1) {
setCounter(counter => counter + step)
}
function decreaseCounter(step = 1) {
setCounter(counter => counter - step)
}
return <div>
<button onClick={() => decreaseCounter()}>-</button>
{counter}
<button onClick={() => increaseCounter()}>+</button>
</div>
}
ReactDOM.render(
<Counter />,
document.getElementById('root')
);
function useCounter(initVal) {
const [counter, setCounter] = useState(initVal)
function increaseCounter(step = 1) {
setCounter(counter => counter + step)
}
function decreaseCounter(step = 1) {
setCounter(counter => counter - step)
}
return { counter, increaseCounter, decreaseCounter }
}
function Counter() {
const {counter, increaseCounter, decreaseCounter} = useCounter(0)
return <div>
<button onClick={() => decreaseCounter()}>-</button>
{counter}
<button onClick={() => increaseCounter()}>+</button>
</div>
}
function App() {
return <>
<Counter />
<Counter />
</>
}
Context
const CounterContext = React.createContext()
function CounterProvider({children}) {
const counterInstance = useCounter(0)
return <CounterContext.Provider value={counterInstance} children={children} />
}
function Counter() {
const {counter, increaseCounter, decreaseCounter} = useContext(CounterContext)
return <div>
<button onClick={() => decreaseCounter()}>-</button>
{counter}
<button onClick={() => increaseCounter()}>+</button>
</div>
}
function App() {
return (
<CounterProvider>
<Counter />
<Counter />
</CounterProvider>
)
}
function useStepper(initVal) {
const [step, setStep] = useState(initVal)
return { step, setStep }
}
const StepContext = React.createContext()
function StepProvider({children}) {
const stepInstance = useStepper(1)
return <StepContext.Provider value={stepInstance} children={children} />
}
function Counter() {
const {step, setStep} = useContext(StepContext)
const {counter, increaseCounter, decreaseCounter} = useContext(CounterContext)
return <div>
<button onClick={() => increaseCounter(step)}>+</button>
{counter}
<button onClick={() => decreaseCounter(step)}>-</button>
</div>
}
function App() {
return (
<StepProvider>
<CounterProvider>
<Counter />
<Counter />
</CounterProvider>
</StepProvider>
)
}
Provider-ek sorrendje?
function Counter() {
const {step, setStep} = useContext(StepContext)
const {counter, increaseCounter, decreaseCounter} = useContext(CounterContext)
const increase = () => {
const exp = Math.floor(Math.log10(counter))
const s = 10**exp
setStep(s)
increaseCounter(s)
}
const decrease = () => {
const exp = Math.ceil(Math.log10(counter)) - 1
const s = 10**exp
setStep(s)
decreaseCounter(s)
}
return <div>
<button onClick={decrease}>-</button>
{counter}
<button onClick={increase}>+</button>
</div>
}
const ActionContext = React.createContext()
function ActionProvider({children}) {
const {step, setStep} = useContext(StepContext)
const {counter, increaseCounter, decreaseCounter} = useContext(CounterContext)
const increase = () => {
const exp = Math.floor(Math.log10(counter))
const s = 10**exp
setStep(s)
increaseCounter(s)
}
const decrease = () => {
const exp = Math.ceil(Math.log10(counter)) - 1
const s = 10**exp
setStep(s)
decreaseCounter(s)
}
const actions = {increase, decrease}
return <ActionContext.Provider value={actions} children={children} />
}
function Counter() {
const {increase, decrease} = useContext(ActionContext)
const {counter} = useContext(CounterContext)
return <div>
<button onClick={decrease}>-</button>
{counter}
<button onClick={increase}>+</button>
</div>
}
const ActionContext = React.createContext()
function ActionProvider({children}) {
const {step, setStep} = useContext(StepContext)
const {counter, increaseCounter, decreaseCounter} = useContext(CounterContext)
const increase = useCallback(() => {
const exp = Math.floor(Math.log10(counter))
const s = 10**exp
setStep(s)
increaseCounter(s)
}, [counter])
const decrease = useCallback(() => {
const exp = Math.ceil(Math.log10(counter)) - 1
const s = 10**exp
setStep(s)
decreaseCounter(s)
}, [counter])
const actions = useMemo(() => ({ increase, decrease}), [counter])
return <ActionContext.Provider value={actions} children={children} />
}
Most egyelőre nem fontos; useReducer
használatával még elegánsabb
useState
useState
) és metódus egységbezárása: saját
hookApp
szinten + prop
drillinguseContext
), de a sorrend
fontosFüggetlenül az adat helyétől
Kitekintés
localStorage
sessionStorage
{title: 'Track1', color: 'red', filters: [1, 3]}
npm install --save nedb
import Nedb from "nedb/browser-version/out/nedb.min";
const db = new Nedb({ filename: 'data.nedb', autoload: true });
//Inserting document(s)
db.insert(doc, function (err, newDoc) { // Callback is optional
// newDoc is the newly inserted document, including its _id
// newDoc has no key called notToBeSaved since its value was undefined
});
//Finding document(s)
db.find({ foo: 'bar' }, function (err, docs) {
// docs is an array containing documents Mars, Earth, Jupiter
// If no document is found, docs is equal to []
});
db.findOne({ _id: 'id1' }, function (err, doc) {
// doc is the document Mars
// If no document is found, doc is null
});
//Replacing a document
db.update({ planet: 'Jupiter' }, { planet: 'Pluton'}, {}, function (err, numReplaced) {
// numReplaced = 1
// The doc #3 has been replaced by { _id: 'id3', planet: 'Pluton' }
// Note that the _id is kept unchanged, and the document has been replaced
});
//Deleting a document
db.remove({ _id: 'id2' }, {}, function (err, numRemoved) {
// numRemoved = 1
});
import Nedb from "nedb/browser-version/out/nedb.min";
export const planetsDb = new Nedb({ filename: 'data.nedb', autoload: true });
import { planetsDb } from "./data/planets";
function App() {
const [planets, setPlanets] = useState([])
useEffect(() => {
planetsDb.find({}, function (err, docs) {
setPlanets(docs)
})
}, [])
return (
<ul>
{planets.map(planet => <li key={planet._id}>{planet.planet}</li>)}
</ul>
);
}
class TrackStorage {
constructor() { /* ... */ }
async find(filter = {}) { /* ... */ }
async insert(track) { /* ... */ }
async update(id, track) { /* ... */ }
async remove(id) { /* ... */ }
}
const trackStorage = new TrackStorage()
const tracks = await trackStorage.find()
await trackStorage.insert({title: 'Track1', color: 'red', filters: [1, 3]})
const findTracks = (filter = {}) => {
return new Promise((resolve, reject) => {
tracksDb.find(filter, function (err, docs) {
if (err) { reject(err) }
else { resolve(docs) }
})
})
}
const promisify = fn => (...args) => {
return new Promise((resolve, reject) => {
fn(...args, (err, data) => {
if (err) {
reject(err)
} else {
resolve(data);
}
});
});
}
// For example
const findTracks = promisify(tracksDb.find)
class TrackStorage {
constructor(tracksDb) {
this.tracksDb = {}
const methods = ['find', 'findOne', 'insert', 'update', 'remove']
methods.forEach(method => this.tracksDb[method] = promisify(tracksDb[method].bind(tracksDb)))
}
async find(filter = {}) {
return await this.tracksDb.find(filter)
}
async insert(track) {
return await this.tracksDb.insert(track)
}
async update(id, track) {
return await this.tracksDb.update({_id: id}, track, { returnUpdatedDocs: true })
}
async remove(id) {
return await this.tracksDb.remove({_id: id})
}
}
const [tracks, setTracks] = useState([])
const addNewTrack = async track => {
const newTrack = await tracksStorage.create(track)
setTracks([...tracks, newTrack])
}
React.Component
React.PureComponent
React.memo
useMemo
useCallback
shouldComponentUpdate(nextProps, nextState) {
return true;
}
class CounterButton extends React.Component {
constructor(props) {
super(props);
this.state = {count: 1};
}
shouldComponentUpdate(nextProps, nextState) {
if (this.props.color !== nextProps.color) {
return true;
}
if (this.state.count !== nextState.count) {
return true;
}
return false;
}
render() {
return (
<button
color={this.props.color}
onClick={() => this.setState(state => ({count: state.count + 1}))}>
Count: {this.state.count}
</button>
);
}
}
Sekély összehasonlítás
class CounterButton extends React.PureComponent {
constructor(props) {
super(props);
this.state = {count: 1};
}
render() {
return (
<button
color={this.props.color}
onClick={() => this.setState(state => ({count: state.count + 1}))}>
Count: {this.state.count}
</button>
);
}
}
const MyComponent = React.memo(function MyComponent(props) {
/* render using props */
});
memo
, PureComponent
import React, { memo } from 'react';
// 🙅♀️ if parent renders, it renders too
const ComponentB = (props) => {
return <div>{props.propB}</div>
};
// 🙆♂️ changes when props changes
const ComponentB = memo((props) => {
return <div>{props.propB}</div>
});
class LoggingButton extends React.Component {
handleClick() {
console.log('this is:', this);
}
render() {
return (
<button onClick={() => this.handleClick()}>
Click me
</button>
);
}
}
class LoggingButton extends React.Component {
handleClick = () => {
console.log('this is:', this);
}
render() {
return (
<button onClick={this.handleClick}>
Click me
</button>
);
}
}
Névtelen függvények
function Foo() {
return (
<button onClick={() => console.log('boop')}> // 😕
BOOP
</button>
);
}
function Foo() {
const handleClick = () => console.log('boop') // 😕
return (
<button onClick={handleClick}>
BOOP
</button>
);
}
Névtelen függvények
const handleClick = () => console.log('boop');
function Foo() {
return (
<button onClick={handleClick}> // 😀
BOOP
</button>
);
}
function Foo() {
const handleClick = useCallback(() => console.log('boop'), []);
return (
<button onClick={handleClick}> // 😀
BOOP
</button>
);
}
Objektumliterál, memo
sem segít
const ComponentA = () => (
<div>
<ComponentB style={{ color: 'blue', background: 'gold' }}/> // 😕
</div>
)
const ComponentB = (props) => (
<div style={this.props.style}>Something</div>
)
const myStyle = { color: 'blue', background: 'gold' } // 😀
const ComponentA = () => (
<div>
<ComponentB style={myStyle}/>
</div>
)
const ComponentB = (props) => (
<div style={this.props.style}>Something</div>
)
const twice = (f, v) => f(f(v));
const add3 = v => v + 3;
twice(add3, 7); // 13
// Függvénygenerátor
function greaterThan(n) {
return m => m > n;
}
let greaterThan10 = greaterThan(10);
console.log(greaterThan10(11)); // true
function unless(test, then) {
if (!test) then();
}
unless(0 % 2 === 1, () => {
console.log(n, "is even");
});
connect
const EnhancedComponent = higherOrderComponent(WrappedComponent);
class BlogPost extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.state = {
blogPost: DataSource.getBlogPost(props.id)
};
}
componentDidMount() {
DataSource.addChangeListener(this.handleChange);
}
componentWillUnmount() {
DataSource.removeChangeListener(this.handleChange);
}
handleChange() {
this.setState({
blogPost: DataSource.getBlogPost(this.props.id)
});
}
render() {
return <TextBlock text={this.state.blogPost} />;
}
}
const BlogPostWithSubscription = withSubscription(
BlogPost,
(DataSource, props) => DataSource.getBlogPost(props.id)
);
// This function takes a component...
function withSubscription(WrappedComponent, selectData) {
// ...and returns another component...
return class extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.state = {
data: selectData(DataSource, props)
};
}
componentDidMount() {
// ... that takes care of the subscription...
DataSource.addChangeListener(this.handleChange);
}
componentWillUnmount() {
DataSource.removeChangeListener(this.handleChange);
}
handleChange() {
this.setState({
data: selectData(DataSource, this.props)
});
}
render() {
// ... and renders the wrapped component with the fresh data!
// Notice that we pass through any additional props
return <WrappedComponent data={this.state.data} {...this.props} />;
}
};
}